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"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
from .microscope import Microscope
|
from .microscope import Microscope
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
__all__ = ['utilities']
|
__all__ = ["utilities"]
|
||||||
|
|
||||||
from . import utilities
|
from . import utilities
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import (
|
from flask import Flask, jsonify, send_file
|
||||||
Flask, jsonify, send_file)
|
|
||||||
|
|
||||||
from serial import SerialException
|
from serial import SerialException
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -39,7 +38,7 @@ from openflexure_microscope.stage.mock import MockStage
|
||||||
# Handle logging
|
# Handle logging
|
||||||
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||||
|
|
||||||
DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, 'openflexure_microscope.log')
|
DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, "openflexure_microscope.log")
|
||||||
|
|
||||||
if (__name__ == "__main__") or (not is_gunicorn):
|
if (__name__ == "__main__") or (not is_gunicorn):
|
||||||
# If imported, but not by gunicorn
|
# If imported, but not by gunicorn
|
||||||
|
|
@ -48,18 +47,15 @@ if (__name__ == "__main__") or (not is_gunicorn):
|
||||||
else:
|
else:
|
||||||
# Direct standard Python logging to file and console
|
# Direct standard Python logging to file and console
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s")
|
error_formatter = logging.Formatter(
|
||||||
|
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
||||||
rotating_logfile = logging.handlers.RotatingFileHandler(
|
|
||||||
DEFAULT_LOGFILE,
|
|
||||||
maxBytes=1000000,
|
|
||||||
backupCount=7
|
|
||||||
)
|
)
|
||||||
|
|
||||||
error_handlers = [
|
rotating_logfile = logging.handlers.RotatingFileHandler(
|
||||||
rotating_logfile,
|
DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7
|
||||||
logging.StreamHandler()
|
)
|
||||||
]
|
|
||||||
|
error_handlers = [rotating_logfile, logging.StreamHandler()]
|
||||||
|
|
||||||
for handler in error_handlers:
|
for handler in error_handlers:
|
||||||
handler.setFormatter(error_formatter)
|
handler.setFormatter(error_formatter)
|
||||||
|
|
@ -88,7 +84,7 @@ def uri(suffix, api_version, base=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.url_map.strict_slashes = False
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
CORS(app, resources=r'/api/*')
|
CORS(app, resources=r"/api/*")
|
||||||
|
|
||||||
# Make errors more API friendly
|
# Make errors more API friendly
|
||||||
handler = JSONExceptionHandler(app)
|
handler = JSONExceptionHandler(app)
|
||||||
|
|
@ -121,10 +117,7 @@ def attach_microscope():
|
||||||
|
|
||||||
# Attach devices to microscope
|
# Attach devices to microscope
|
||||||
logging.debug("Attaching devices to microscope...")
|
logging.debug("Attaching devices to microscope...")
|
||||||
api_microscope.attach(
|
api_microscope.attach(api_camera, api_stage)
|
||||||
api_camera,
|
|
||||||
api_stage
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restore loaded capture array to camera object
|
# Restore loaded capture array to camera object
|
||||||
logging.debug("Restoring captures...")
|
logging.debug("Restoring captures...")
|
||||||
|
|
@ -140,26 +133,26 @@ def attach_microscope():
|
||||||
|
|
||||||
# Base routes
|
# Base routes
|
||||||
base_blueprint = blueprints.base.construct_blueprint(api_microscope)
|
base_blueprint = blueprints.base.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1'))
|
app.register_blueprint(base_blueprint, url_prefix=uri("", "v1"))
|
||||||
|
|
||||||
# Stage routes
|
# Stage routes
|
||||||
stage_blueprint = blueprints.stage.construct_blueprint(api_microscope)
|
stage_blueprint = blueprints.stage.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1'))
|
app.register_blueprint(stage_blueprint, url_prefix=uri("/stage", "v1"))
|
||||||
|
|
||||||
# Camera routes
|
# Camera routes
|
||||||
camera_blueprint = blueprints.camera.construct_blueprint(api_microscope)
|
camera_blueprint = blueprints.camera.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1'))
|
app.register_blueprint(camera_blueprint, url_prefix=uri("/camera", "v1"))
|
||||||
|
|
||||||
# Plugin routes
|
# Plugin routes
|
||||||
plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope)
|
plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1'))
|
app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1"))
|
||||||
|
|
||||||
# Task routes
|
# Task routes
|
||||||
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
|
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1'))
|
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/routes')
|
@app.route("/routes")
|
||||||
def routes():
|
def routes():
|
||||||
"""
|
"""
|
||||||
List of all connected API routes
|
List of all connected API routes
|
||||||
|
|
@ -173,7 +166,7 @@ def routes():
|
||||||
return jsonify(list_routes(app))
|
return jsonify(list_routes(app))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log')
|
@app.route("/log")
|
||||||
def err_log():
|
def err_log():
|
||||||
"""
|
"""
|
||||||
Most recent 1mb of log output
|
Most recent 1mb of log output
|
||||||
|
|
@ -188,7 +181,7 @@ def err_log():
|
||||||
return send_file(
|
return send_file(
|
||||||
DEFAULT_LOGFILE,
|
DEFAULT_LOGFILE,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename='openflexure_microscope_{}.log'.format(timestamp)
|
attachment_filename="openflexure_microscope_{}.log".format(timestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -219,4 +212,4 @@ def cleanup():
|
||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
|
app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,14 @@ class JSONExceptionHandler(object):
|
||||||
def std_handler(self, error):
|
def std_handler(self, error):
|
||||||
if isinstance(error, HTTPException):
|
if isinstance(error, HTTPException):
|
||||||
message = error.description
|
message = error.description
|
||||||
elif hasattr(error, 'message'):
|
elif hasattr(error, "message"):
|
||||||
message = error.message
|
message = error.message
|
||||||
else:
|
else:
|
||||||
message = str(error)
|
message = str(error)
|
||||||
|
|
||||||
status_code = error.code if isinstance(error, HTTPException) else 500
|
status_code = error.code if isinstance(error, HTTPException) else 500
|
||||||
|
|
||||||
response = {
|
response = {"status_code": status_code, "message": escape(message)}
|
||||||
'status_code': status_code,
|
|
||||||
'message': escape(message)
|
|
||||||
}
|
|
||||||
return jsonify(response), status_code
|
return jsonify(response), status_code
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ class JsonResponse:
|
||||||
"""
|
"""
|
||||||
# Try to load as json
|
# Try to load as json
|
||||||
try:
|
try:
|
||||||
self.json = request.get_json() #: dict: Dictionary representation of request JSON
|
self.json = (
|
||||||
|
request.get_json()
|
||||||
|
) #: dict: Dictionary representation of request JSON
|
||||||
# If malformed JSON is passed, make an empty dictionary
|
# If malformed JSON is passed, make an empty dictionary
|
||||||
except BadRequest as e:
|
except BadRequest as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
|
|
@ -55,15 +57,12 @@ def gen(camera):
|
||||||
# the obtained frame is a jpeg
|
# the obtained frame is a jpeg
|
||||||
frame = camera.get_frame()
|
frame = camera.get_frame()
|
||||||
|
|
||||||
yield (b'--frame\r\n'
|
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
|
||||||
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
|
||||||
|
|
||||||
|
|
||||||
def get_bool(get_arg):
|
def get_bool(get_arg):
|
||||||
"""Convert GET request argument string to a Python bool"""
|
"""Convert GET request argument string to a Python bool"""
|
||||||
if (get_arg == 'true' or
|
if get_arg == "true" or get_arg == "True" or get_arg == "1":
|
||||||
get_arg == 'True' or
|
|
||||||
get_arg == '1'):
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
@ -80,10 +79,7 @@ def list_routes(app):
|
||||||
endpoint = rule.endpoint
|
endpoint = rule.endpoint
|
||||||
methods = list(rule.methods)
|
methods = list(rule.methods)
|
||||||
url = url_for(rule.endpoint, **options)
|
url = url_for(rule.endpoint, **options)
|
||||||
line = {
|
line = {"endpoint": endpoint, "methods": methods}
|
||||||
'endpoint': endpoint,
|
|
||||||
'methods': methods
|
|
||||||
}
|
|
||||||
output[url] = line
|
output[url] = line
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
@ -6,7 +6,6 @@ import logging
|
||||||
|
|
||||||
|
|
||||||
class StreamAPI(MicroscopeView):
|
class StreamAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Real-time MJPEG stream from the microscope camera
|
Real-time MJPEG stream from the microscope camera
|
||||||
|
|
@ -22,11 +21,11 @@ class StreamAPI(MicroscopeView):
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
gen(self.microscope.camera),
|
gen(self.microscope.camera),
|
||||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StateAPI(MicroscopeView):
|
class StateAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
JSON representation of the microscope object.
|
JSON representation of the microscope object.
|
||||||
|
|
@ -77,7 +76,6 @@ class StateAPI(MicroscopeView):
|
||||||
|
|
||||||
|
|
||||||
class ConfigAPI(MicroscopeView):
|
class ConfigAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
JSON representation of the microscope config.
|
JSON representation of the microscope config.
|
||||||
|
|
@ -216,21 +214,18 @@ class ConfigAPI(MicroscopeView):
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
blueprint = Blueprint('base_blueprint', __name__)
|
blueprint = Blueprint("base_blueprint", __name__)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/stream',
|
"/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj)
|
||||||
view_func=StreamAPI.as_view('stream', microscope=microscope_obj)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/state',
|
"/state", view_func=StateAPI.as_view("state", microscope=microscope_obj)
|
||||||
view_func=StateAPI.as_view('state', microscope=microscope_obj)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/config',
|
"/config", view_func=ConfigAPI.as_view("config", microscope=microscope_obj)
|
||||||
view_func=ConfigAPI.as_view('config', microscope=microscope_obj)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return blueprint
|
return blueprint
|
||||||
|
|
|
||||||
|
|
@ -7,51 +7,70 @@ from . import capture, record, preview, function
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
blueprint = Blueprint('camera_blueprint', __name__)
|
blueprint = Blueprint("camera_blueprint", __name__)
|
||||||
|
|
||||||
# Metadata routes
|
# Metadata routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/metadata/<filename>',
|
"/capture/<capture_id>/metadata/<filename>",
|
||||||
view_func=capture.MetadataAPI.as_view('metadata_download', microscope=microscope_obj))
|
view_func=capture.MetadataAPI.as_view(
|
||||||
|
"metadata_download", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/metadata',
|
"/capture/<capture_id>/metadata",
|
||||||
view_func=capture.MetadataRedirectAPI.as_view('metadata_download_redirect', microscope=microscope_obj))
|
view_func=capture.MetadataRedirectAPI.as_view(
|
||||||
|
"metadata_download_redirect", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Tag routes
|
# Tag routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/tags',
|
"/capture/<capture_id>/tags",
|
||||||
view_func=capture.TagsAPI.as_view('capture_tags', microscope=microscope_obj))
|
view_func=capture.TagsAPI.as_view("capture_tags", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
# Capture routes
|
# Capture routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/download/<filename>',
|
"/capture/<capture_id>/download/<filename>",
|
||||||
view_func=capture.DownloadAPI.as_view('capture_download', microscope=microscope_obj))
|
view_func=capture.DownloadAPI.as_view(
|
||||||
|
"capture_download", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/download',
|
"/capture/<capture_id>/download",
|
||||||
view_func=capture.DownloadRedirectAPI.as_view('capture_download_redirect', microscope=microscope_obj))
|
view_func=capture.DownloadRedirectAPI.as_view(
|
||||||
|
"capture_download_redirect", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/<capture_id>/',
|
"/capture/<capture_id>/",
|
||||||
view_func=capture.CaptureAPI.as_view('capture', microscope=microscope_obj))
|
view_func=capture.CaptureAPI.as_view("capture", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/capture/',
|
"/capture/",
|
||||||
view_func=capture.ListAPI.as_view('capture_list', microscope=microscope_obj))
|
view_func=capture.ListAPI.as_view("capture_list", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
# Preview routes
|
# Preview routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/preview/<string:operation>',
|
"/preview/<string:operation>",
|
||||||
view_func=preview.GPUPreviewAPI.as_view('gpu_preview', microscope=microscope_obj))
|
view_func=preview.GPUPreviewAPI.as_view(
|
||||||
|
"gpu_preview", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Function routes
|
# Function routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/overlay',
|
"/overlay",
|
||||||
view_func=function.OverlayAPI.as_view('overlay', microscope=microscope_obj))
|
view_func=function.OverlayAPI.as_view("overlay", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/zoom',
|
"/zoom", view_func=function.ZoomAPI.as_view("zoom", microscope=microscope_obj)
|
||||||
view_func=function.ZoomAPI.as_view('zoom', microscope=microscope_obj))
|
)
|
||||||
|
|
||||||
return(blueprint)
|
return blueprint
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from flask import Response, jsonify, request, abort, url_for, redirect, send_fil
|
||||||
|
|
||||||
|
|
||||||
class ListAPI(MicroscopeView):
|
class ListAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Get list of image captures.
|
Get list of image captures.
|
||||||
|
|
@ -30,12 +29,16 @@ class ListAPI(MicroscopeView):
|
||||||
:status 200: capture found
|
:status 200: capture found
|
||||||
:status 404: no capture found with that id
|
:status 404: no capture found with that id
|
||||||
"""
|
"""
|
||||||
include_unavailable = get_bool(request.args.get('include_unavailable'))
|
include_unavailable = get_bool(request.args.get("include_unavailable"))
|
||||||
|
|
||||||
if include_unavailable:
|
if include_unavailable:
|
||||||
captures = [image.state for image in self.microscope.camera.images]
|
captures = [image.state for image in self.microscope.camera.images]
|
||||||
else:
|
else:
|
||||||
captures = [image.state for image in self.microscope.camera.images if image.state['available']]
|
captures = [
|
||||||
|
image.state
|
||||||
|
for image in self.microscope.camera.images
|
||||||
|
if image.state["available"]
|
||||||
|
]
|
||||||
|
|
||||||
return jsonify(captures)
|
return jsonify(captures)
|
||||||
|
|
||||||
|
|
@ -100,38 +103,40 @@ class ListAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
filename = payload.param('filename')
|
filename = payload.param("filename")
|
||||||
temporary = payload.param('temporary', default=False, convert=bool)
|
temporary = payload.param("temporary", default=False, convert=bool)
|
||||||
use_video_port = payload.param('use_video_port', default=False, convert=bool)
|
use_video_port = payload.param("use_video_port", default=False, convert=bool)
|
||||||
bayer = payload.param('bayer', default=True, convert=bool)
|
bayer = payload.param("bayer", default=True, convert=bool)
|
||||||
metadata = payload.param('metadata', default={}, convert=dict)
|
metadata = payload.param("metadata", default={}, convert=dict)
|
||||||
tags = payload.param('tags', default=[], convert=list)
|
tags = payload.param("tags", default=[], convert=list)
|
||||||
|
|
||||||
resize = payload.param('size', default=None)
|
resize = payload.param("size", default=None)
|
||||||
if resize:
|
if resize:
|
||||||
if ('width' in resize) and ('height' in resize):
|
if ("width" in resize) and ("height" in resize):
|
||||||
resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple
|
resize = (
|
||||||
|
int(resize["width"]),
|
||||||
|
int(resize["height"]),
|
||||||
|
) # Convert dict to tuple
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
|
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
|
||||||
with self.microscope.camera.lock:
|
with self.microscope.camera.lock:
|
||||||
output = self.microscope.camera.new_image(
|
output = self.microscope.camera.new_image(
|
||||||
write_to_file=True,
|
write_to_file=True, temporary=temporary, filename=filename
|
||||||
temporary=temporary,
|
)
|
||||||
filename=filename)
|
|
||||||
|
|
||||||
self.microscope.camera.capture(
|
self.microscope.camera.capture(
|
||||||
output,
|
output, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||||
use_video_port=use_video_port,
|
)
|
||||||
resize=resize,
|
|
||||||
bayer=bayer)
|
|
||||||
|
|
||||||
metadata.update({
|
metadata.update(
|
||||||
'position': self.microscope.state['stage']['position'],
|
{
|
||||||
'microscope_id': self.microscope.id,
|
"position": self.microscope.state["stage"]["position"],
|
||||||
'microscope_name': self.microscope.name
|
"microscope_id": self.microscope.id,
|
||||||
})
|
"microscope_name": self.microscope.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
output.put_metadata(metadata)
|
output.put_metadata(metadata)
|
||||||
output.put_tags(tags)
|
output.put_tags(tags)
|
||||||
|
|
@ -140,7 +145,6 @@ class ListAPI(MicroscopeView):
|
||||||
|
|
||||||
|
|
||||||
class CaptureAPI(MicroscopeView):
|
class CaptureAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self, capture_id):
|
def get(self, capture_id):
|
||||||
"""
|
"""
|
||||||
Get JSON representation of a capture
|
Get JSON representation of a capture
|
||||||
|
|
@ -200,14 +204,15 @@ class CaptureAPI(MicroscopeView):
|
||||||
|
|
||||||
# Add API routes to returned state
|
# Add API routes to returned state
|
||||||
uri_dict = {
|
uri_dict = {
|
||||||
'uri': {'state': '{}'.format(url_for('.capture', capture_id=capture_obj.id))}
|
"uri": {
|
||||||
|
"state": "{}".format(url_for(".capture", capture_id=capture_obj.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# If available, also add download link
|
# If available, also add download link
|
||||||
if capture_metadata['available']:
|
if capture_metadata["available"]:
|
||||||
uri_dict['uri']['download'] = '{}download/{}'.format(
|
uri_dict["uri"]["download"] = "{}download/{}".format(
|
||||||
url_for('.capture', capture_id=capture_obj.id),
|
url_for(".capture", capture_id=capture_obj.id), capture_obj.filename
|
||||||
capture_obj.filename
|
|
||||||
)
|
)
|
||||||
|
|
||||||
capture_metadata.update(uri_dict)
|
capture_metadata.update(uri_dict)
|
||||||
|
|
@ -299,17 +304,20 @@ class DownloadRedirectAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
thumbnail = get_bool(request.args.get('thumbnail'))
|
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||||
|
|
||||||
return redirect(url_for(
|
return redirect(
|
||||||
'.capture_download',
|
url_for(
|
||||||
capture_id=capture_id,
|
".capture_download",
|
||||||
filename=capture_obj.filename,
|
capture_id=capture_id,
|
||||||
thumbnail=thumbnail
|
filename=capture_obj.filename,
|
||||||
), code=307)
|
thumbnail=thumbnail,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DownloadAPI(MicroscopeView):
|
class DownloadAPI(MicroscopeView):
|
||||||
|
|
@ -317,19 +325,22 @@ class DownloadAPI(MicroscopeView):
|
||||||
|
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
thumbnail = get_bool(request.args.get('thumbnail'))
|
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||||
|
|
||||||
# If no filename is specified, redirect to the capture's currently set filename
|
# If no filename is specified, redirect to the capture's currently set filename
|
||||||
if not filename:
|
if not filename:
|
||||||
return redirect(url_for(
|
return redirect(
|
||||||
'capture_download',
|
url_for(
|
||||||
capture_id=capture_id,
|
"capture_download",
|
||||||
filename=capture_obj.filename,
|
capture_id=capture_id,
|
||||||
thumbnail=thumbnail
|
filename=capture_obj.filename,
|
||||||
), code=307)
|
thumbnail=thumbnail,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
# Download the image data using the requested filename
|
# Download the image data using the requested filename
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
|
|
@ -337,9 +348,7 @@ class DownloadAPI(MicroscopeView):
|
||||||
else:
|
else:
|
||||||
img = capture_obj.data
|
img = capture_obj.data
|
||||||
|
|
||||||
return send_file(
|
return send_file(img, mimetype="image/jpeg")
|
||||||
img,
|
|
||||||
mimetype='image/jpeg')
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataRedirectAPI(MicroscopeView):
|
class MetadataRedirectAPI(MicroscopeView):
|
||||||
|
|
@ -374,14 +383,17 @@ class MetadataRedirectAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
return redirect(url_for(
|
return redirect(
|
||||||
'.metadata_download',
|
url_for(
|
||||||
capture_id=capture_id,
|
".metadata_download",
|
||||||
filename=capture_obj.metadataname
|
capture_id=capture_id,
|
||||||
), code=307)
|
filename=capture_obj.metadataname,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MetadataAPI(MicroscopeView):
|
class MetadataAPI(MicroscopeView):
|
||||||
|
|
@ -389,23 +401,24 @@ class MetadataAPI(MicroscopeView):
|
||||||
|
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
# If no filename is specified, redirect to the capture's currently set filename
|
# If no filename is specified, redirect to the capture's currently set filename
|
||||||
if not filename:
|
if not filename:
|
||||||
return redirect(url_for(
|
return redirect(
|
||||||
'capture_download',
|
url_for(
|
||||||
capture_id=capture_id,
|
"capture_download",
|
||||||
filename=capture_obj.metadataname
|
capture_id=capture_id,
|
||||||
), code=307)
|
filename=capture_obj.metadataname,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
# Download the metadata using the requested filename
|
# Download the metadata using the requested filename
|
||||||
data = capture_obj.yaml
|
data = capture_obj.yaml
|
||||||
|
|
||||||
return Response(
|
return Response(data, mimetype="text/yaml")
|
||||||
data,
|
|
||||||
mimetype="text/yaml")
|
|
||||||
|
|
||||||
|
|
||||||
class TagsAPI(MicroscopeView):
|
class TagsAPI(MicroscopeView):
|
||||||
|
|
@ -430,10 +443,10 @@ class TagsAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"])
|
||||||
|
|
||||||
return jsonify(metadata_tags)
|
return jsonify(metadata_tags)
|
||||||
|
|
||||||
|
|
@ -460,7 +473,7 @@ class TagsAPI(MicroscopeView):
|
||||||
|
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
if not capture_obj or not capture_obj.state['available']:
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
return abort(404) # 404 Not Found
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
data_dict = JsonResponse(request).json
|
data_dict = JsonResponse(request).json
|
||||||
|
|
@ -470,7 +483,7 @@ class TagsAPI(MicroscopeView):
|
||||||
|
|
||||||
capture_obj.put_tags(data_dict)
|
capture_obj.put_tags(data_dict)
|
||||||
|
|
||||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"])
|
||||||
|
|
||||||
return jsonify(metadata_tags)
|
return jsonify(metadata_tags)
|
||||||
|
|
||||||
|
|
@ -507,6 +520,6 @@ class TagsAPI(MicroscopeView):
|
||||||
for tag in data_dict:
|
for tag in data_dict:
|
||||||
capture_obj.delete_tag(str(tag))
|
capture_obj.delete_tag(str(tag))
|
||||||
|
|
||||||
metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags'])
|
metadata_tags = filter_dict(capture_obj.state, ["metadata", "tags"])
|
||||||
|
|
||||||
return jsonify(metadata_tags)
|
return jsonify(metadata_tags)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from flask import jsonify, request
|
||||||
|
|
||||||
|
|
||||||
class ZoomAPI(MicroscopeView):
|
class ZoomAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Get the current zoom value
|
Get the current zoom value
|
||||||
|
|
@ -17,9 +16,9 @@ class ZoomAPI(MicroscopeView):
|
||||||
:<header Content-Type: application/json
|
:<header Content-Type: application/json
|
||||||
:status 200: preview started/stopped
|
:status 200: preview started/stopped
|
||||||
"""
|
"""
|
||||||
zoom_value = self.microscope.camera.state['zoom_value']
|
zoom_value = self.microscope.camera.state["zoom_value"]
|
||||||
|
|
||||||
return jsonify({'zoom_value': zoom_value})
|
return jsonify({"zoom_value": zoom_value})
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -44,7 +43,7 @@ class ZoomAPI(MicroscopeView):
|
||||||
:status 200: preview started/stopped
|
:status 200: preview started/stopped
|
||||||
"""
|
"""
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
zoom_value = payload.param('zoom_value', default=1.0, convert=float)
|
zoom_value = payload.param("zoom_value", default=1.0, convert=float)
|
||||||
|
|
||||||
self.microscope.camera.set_zoom(zoom_value)
|
self.microscope.camera.set_zoom(zoom_value)
|
||||||
|
|
||||||
|
|
@ -52,7 +51,6 @@ class ZoomAPI(MicroscopeView):
|
||||||
|
|
||||||
|
|
||||||
class OverlayAPI(MicroscopeView):
|
class OverlayAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Get overlay text
|
Get overlay text
|
||||||
|
|
@ -67,7 +65,7 @@ class OverlayAPI(MicroscopeView):
|
||||||
text = self.microscope.camera.camera.annotate_text
|
text = self.microscope.camera.camera.annotate_text
|
||||||
size = self.microscope.camera.camera.annotate_text_size
|
size = self.microscope.camera.camera.annotate_text_size
|
||||||
|
|
||||||
return jsonify({'text': text, 'size': size})
|
return jsonify({"text": text, "size": size})
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -94,10 +92,10 @@ class OverlayAPI(MicroscopeView):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
text = payload.param('text', default="", convert=str)
|
text = payload.param("text", default="", convert=str)
|
||||||
size = payload.param('size', default=50, convert=int)
|
size = payload.param("size", default=50, convert=int)
|
||||||
|
|
||||||
self.microscope.camera.camera.annotate_text = text
|
self.microscope.camera.camera.annotate_text = text
|
||||||
self.microscope.camera.camera.annotate_text_size = size
|
self.microscope.camera.camera.annotate_text_size = size
|
||||||
|
|
||||||
return jsonify({'text': text, 'size': size})
|
return jsonify({"text": text, "size": size})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import logging
|
||||||
|
|
||||||
|
|
||||||
class GPUPreviewAPI(MicroscopeView):
|
class GPUPreviewAPI(MicroscopeView):
|
||||||
|
|
||||||
def post(self, operation):
|
def post(self, operation):
|
||||||
"""
|
"""
|
||||||
Start or stop the onboard GPU preview.
|
Start or stop the onboard GPU preview.
|
||||||
|
|
@ -39,7 +38,7 @@ class GPUPreviewAPI(MicroscopeView):
|
||||||
if operation == "start":
|
if operation == "start":
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
window = payload.param('window', default=[])
|
window = payload.param("window", default=[])
|
||||||
logging.debug(window)
|
logging.debug(window)
|
||||||
|
|
||||||
if len(window) != 4:
|
if len(window) != 4:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class PluginFormAPI(MicroscopeView):
|
class PluginFormAPI(MicroscopeView):
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -26,12 +27,12 @@ class PluginFormAPI(MicroscopeView):
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
blueprint = Blueprint('plugin_blueprint', __name__)
|
blueprint = Blueprint("plugin_blueprint", __name__)
|
||||||
|
|
||||||
# Create a base route to return plugin API forms, if any exist
|
# Create a base route to return plugin API forms, if any exist
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/',
|
"/",
|
||||||
view_func=PluginFormAPI.as_view('plugin_api_form', microscope=microscope_obj)
|
view_func=PluginFormAPI.as_view("plugin_api_form", microscope=microscope_obj),
|
||||||
)
|
)
|
||||||
|
|
||||||
all_routes = []
|
all_routes = []
|
||||||
|
|
@ -40,7 +41,7 @@ def construct_blueprint(microscope_obj):
|
||||||
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
|
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
|
||||||
|
|
||||||
# If plugin contains valid endpoints
|
# If plugin contains valid endpoints
|
||||||
if hasattr(plugin_obj, 'api_views') and isinstance(plugin_obj.api_views, dict):
|
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
|
||||||
|
|
||||||
# We'll keep a record of how each route was expanded
|
# We'll keep a record of how each route was expanded
|
||||||
expanded_routes = {}
|
expanded_routes = {}
|
||||||
|
|
@ -50,7 +51,7 @@ def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
# Remove all leading slashes from view route
|
# Remove all leading slashes from view route
|
||||||
cleaned_route = view_route
|
cleaned_route = view_route
|
||||||
while cleaned_route[0] == '/':
|
while cleaned_route[0] == "/":
|
||||||
cleaned_route = cleaned_route[1:]
|
cleaned_route = cleaned_route[1:]
|
||||||
|
|
||||||
# Construct a full view route from the plugin name
|
# Construct a full view route from the plugin name
|
||||||
|
|
@ -61,12 +62,16 @@ def construct_blueprint(microscope_obj):
|
||||||
expanded_routes[view_route] = full_view_route
|
expanded_routes[view_route] = full_view_route
|
||||||
|
|
||||||
# Check if endpoint name clashes
|
# Check if endpoint name clashes
|
||||||
if full_view_route not in all_routes and issubclass(view_class, MicroscopeViewPlugin):
|
if full_view_route not in all_routes and issubclass(
|
||||||
|
view_class, MicroscopeViewPlugin
|
||||||
|
):
|
||||||
# Add route to main route dictionary
|
# Add route to main route dictionary
|
||||||
all_routes.append(full_view_route)
|
all_routes.append(full_view_route)
|
||||||
|
|
||||||
# Create a Python-safe name for the route
|
# Create a Python-safe name for the route
|
||||||
plugin_route_id = 'plugin{}'.format(full_view_route).replace('/', '_')
|
plugin_route_id = "plugin{}".format(full_view_route).replace(
|
||||||
|
"/", "_"
|
||||||
|
)
|
||||||
|
|
||||||
# Add route to the plugins blueprint
|
# Add route to the plugins blueprint
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
|
|
@ -74,29 +79,36 @@ def construct_blueprint(microscope_obj):
|
||||||
view_func=view_class.as_view(
|
view_func=view_class.as_view(
|
||||||
plugin_route_id,
|
plugin_route_id,
|
||||||
microscope=microscope_obj,
|
microscope=microscope_obj,
|
||||||
plugin=plugin_obj
|
plugin=plugin_obj,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"An endpoint /{} has already been loaded. Skipping {}.".format(
|
"An endpoint /{} has already been loaded. Skipping {}.".format(
|
||||||
full_view_route,
|
full_view_route, view_class
|
||||||
view_class
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# If plugin includes an API form
|
# If plugin includes an API form
|
||||||
if hasattr(plugin_obj, 'api_form') and isinstance(plugin_obj.api_form, dict):
|
if hasattr(plugin_obj, "api_form") and isinstance(
|
||||||
|
plugin_obj.api_form, dict
|
||||||
|
):
|
||||||
api_form_info = plugin_obj.api_form
|
api_form_info = plugin_obj.api_form
|
||||||
# TODO: Validate form? We need to make sure no single plugin can break all plugins.
|
# TODO: Validate form? We need to make sure no single plugin can break all plugins.
|
||||||
api_form_info['id'] = plugin_name
|
api_form_info["id"] = plugin_name
|
||||||
if 'forms' in api_form_info and isinstance(api_form_info['forms'], list):
|
if "forms" in api_form_info and isinstance(
|
||||||
for form in api_form_info['forms']:
|
api_form_info["forms"], list
|
||||||
if 'route' in form and form['route'] in expanded_routes.keys():
|
):
|
||||||
form['route'] = expanded_routes[form['route']]
|
for form in api_form_info["forms"]:
|
||||||
|
if "route" in form and form["route"] in expanded_routes.keys():
|
||||||
|
form["route"] = expanded_routes[form["route"]]
|
||||||
else:
|
else:
|
||||||
logging.warn("No valid expandable route found for {}".format(form['route']))
|
logging.warn(
|
||||||
|
"No valid expandable route found for {}".format(
|
||||||
|
form["route"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Store the complete form in Microscope().plugin.form
|
# Store the complete form in Microscope().plugin.form
|
||||||
microscope_obj.plugin.forms.append(api_form_info)
|
microscope_obj.plugin.forms.append(api_form_info)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import logging
|
||||||
|
|
||||||
|
|
||||||
class PositionAPI(MicroscopeView):
|
class PositionAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Return current x, y and z positions of the stage.
|
Return current x, y and z positions of the stage.
|
||||||
|
|
@ -42,7 +41,7 @@ class PositionAPI(MicroscopeView):
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
out = filter_dict(self.microscope.state, ['stage', 'position'])
|
out = filter_dict(self.microscope.state, ["stage", "position"])
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
|
|
@ -67,15 +66,19 @@ class PositionAPI(MicroscopeView):
|
||||||
position = [0, 0, 0]
|
position = [0, 0, 0]
|
||||||
|
|
||||||
# Handle absolute positioning (calculate a relative move from current position and target)
|
# Handle absolute positioning (calculate a relative move from current position and target)
|
||||||
if (payload.param('absolute') is True) and (self.microscope.stage): # Only if stage exists
|
if (payload.param("absolute") is True) and (
|
||||||
target_position = axes_to_array(payload.json, ['x', 'y', 'z'])
|
self.microscope.stage
|
||||||
|
): # Only if stage exists
|
||||||
|
target_position = axes_to_array(payload.json, ["x", "y", "z"])
|
||||||
logging.debug("TARGET: {}".format(target_position))
|
logging.debug("TARGET: {}".format(target_position))
|
||||||
position = [target_position[i] - self.microscope.stage.position[i] for i in range(3)]
|
position = [
|
||||||
|
target_position[i] - self.microscope.stage.position[i] for i in range(3)
|
||||||
|
]
|
||||||
logging.debug("DELTA: {}".format(position))
|
logging.debug("DELTA: {}".format(position))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Get coordinates from payload
|
# Get coordinates from payload
|
||||||
position = axes_to_array(payload.json, ['x', 'y', 'z'], [0, 0, 0])
|
position = axes_to_array(payload.json, ["x", "y", "z"], [0, 0, 0])
|
||||||
|
|
||||||
logging.debug(position)
|
logging.debug(position)
|
||||||
|
|
||||||
|
|
@ -85,18 +88,18 @@ class PositionAPI(MicroscopeView):
|
||||||
with self.microscope.stage.lock:
|
with self.microscope.stage.lock:
|
||||||
self.microscope.stage.move_rel(position)
|
self.microscope.stage.move_rel(position)
|
||||||
|
|
||||||
out = filter_dict(self.microscope.state, ['stage', 'position'])
|
out = filter_dict(self.microscope.state, ["stage", "position"])
|
||||||
|
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
blueprint = Blueprint('stage_blueprint', __name__)
|
blueprint = Blueprint("stage_blueprint", __name__)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/position',
|
"/position",
|
||||||
view_func=PositionAPI.as_view('position', microscope=microscope_obj)
|
view_func=PositionAPI.as_view("position", microscope=microscope_obj),
|
||||||
)
|
)
|
||||||
|
|
||||||
return blueprint
|
return blueprint
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from flask import jsonify, abort, Blueprint
|
||||||
|
|
||||||
|
|
||||||
class TaskListAPI(MicroscopeView):
|
class TaskListAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Get list of long-running tasks.
|
Get list of long-running tasks.
|
||||||
|
|
@ -66,7 +65,6 @@ class TaskListAPI(MicroscopeView):
|
||||||
|
|
||||||
|
|
||||||
class TaskAPI(MicroscopeView):
|
class TaskAPI(MicroscopeView):
|
||||||
|
|
||||||
def get(self, task_id):
|
def get(self, task_id):
|
||||||
"""
|
"""
|
||||||
Get JSON representation of a task
|
Get JSON representation of a task
|
||||||
|
|
@ -123,14 +121,11 @@ class TaskAPI(MicroscopeView):
|
||||||
success = self.microscope.task.delete(task_id)
|
success = self.microscope.task.delete(task_id)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
data = {
|
data = {"status": "success", "message": "task successfully deleted"}
|
||||||
'status': 'success',
|
|
||||||
'message': 'task successfully deleted'
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
data = {
|
data = {
|
||||||
'status': 'fail',
|
"status": "fail",
|
||||||
'message': 'task could not be deleted, as it is currently running or does not exist'
|
"message": "task could not be deleted, as it is currently running or does not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
@ -138,16 +133,14 @@ class TaskAPI(MicroscopeView):
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
blueprint = Blueprint('task_blueprint', __name__)
|
blueprint = Blueprint("task_blueprint", __name__)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/',
|
"/", view_func=TaskListAPI.as_view("task_list", microscope=microscope_obj)
|
||||||
view_func=TaskListAPI.as_view('task_list', microscope=microscope_obj)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
'/<task_id>/',
|
"/<task_id>/", view_func=TaskAPI.as_view("task", microscope=microscope_obj)
|
||||||
view_func=TaskAPI.as_view('task', microscope=microscope_obj)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return blueprint
|
return blueprint
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ class MicroscopeView(MethodView):
|
||||||
Create a generic MethodView with a globally available
|
Create a generic MethodView with a globally available
|
||||||
microscope object passed as an argument.
|
microscope object passed as an argument.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, microscope, **kwargs):
|
def __init__(self, microscope, **kwargs):
|
||||||
|
|
||||||
self.microscope = microscope
|
self.microscope = microscope
|
||||||
|
|
@ -19,6 +20,7 @@ class MicroscopeViewPlugin(MicroscopeView):
|
||||||
microscope object passed as an argument, and a plugin
|
microscope object passed as an argument, and a plugin
|
||||||
reference stored in 'self'. Initially None.
|
reference stored in 'self'. Initially None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, microscope, plugin=None, **kwargs):
|
def __init__(self, microscope, plugin=None, **kwargs):
|
||||||
|
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ class CameraEvent(object):
|
||||||
|
|
||||||
An event-like class that signals all active clients when a new frame is available.
|
An event-like class that signals all active clients when a new frame is available.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.events = {}
|
self.events = {}
|
||||||
|
|
||||||
|
|
@ -112,6 +113,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
images (list): List of image capture objects
|
images (list): List of image capture objects
|
||||||
videos (list): List of video capture objects
|
videos (list): List of video capture objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.thread = None
|
self.thread = None
|
||||||
self.camera = None
|
self.camera = None
|
||||||
|
|
@ -128,10 +130,10 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
self.state = {}
|
self.state = {}
|
||||||
self.paths = {
|
self.paths = {
|
||||||
'image': BASE_CAPTURE_PATH,
|
"image": BASE_CAPTURE_PATH,
|
||||||
'video': BASE_CAPTURE_PATH,
|
"video": BASE_CAPTURE_PATH,
|
||||||
'image_tmp': TEMP_CAPTURE_PATH,
|
"image_tmp": TEMP_CAPTURE_PATH,
|
||||||
'video_tpm': TEMP_CAPTURE_PATH
|
"video_tpm": TEMP_CAPTURE_PATH,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Capture data
|
# Capture data
|
||||||
|
|
@ -176,7 +178,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.last_access = time.time()
|
self.last_access = time.time()
|
||||||
self.stop = False
|
self.stop = False
|
||||||
|
|
||||||
if not self.state['stream_active']:
|
if not self.state["stream_active"]:
|
||||||
# start background frame thread
|
# start background frame thread
|
||||||
self.thread = threading.Thread(target=self._thread)
|
self.thread = threading.Thread(target=self._thread)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
|
|
@ -196,12 +198,12 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
logging.debug("Stopping worker thread")
|
logging.debug("Stopping worker thread")
|
||||||
timeout_time = time.time() + timeout
|
timeout_time = time.time() + timeout
|
||||||
|
|
||||||
if self.state['stream_active']:
|
if self.state["stream_active"]:
|
||||||
self.stop = True
|
self.stop = True
|
||||||
self.thread.join() # Wait for stream thread to exit
|
self.thread.join() # Wait for stream thread to exit
|
||||||
logging.debug("Waiting for stream thread to exit.")
|
logging.debug("Waiting for stream thread to exit.")
|
||||||
|
|
||||||
while self.state['stream_active']:
|
while self.state["stream_active"]:
|
||||||
if time.time() > timeout_time:
|
if time.time() > timeout_time:
|
||||||
logging.debug("Timeout waiting for worker thread close.")
|
logging.debug("Timeout waiting for worker thread close.")
|
||||||
raise TimeoutError("Timeout waiting for worker thread close.")
|
raise TimeoutError("Timeout waiting for worker thread close.")
|
||||||
|
|
@ -249,12 +251,13 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
# CREATING NEW CAPTURES
|
# CREATING NEW CAPTURES
|
||||||
|
|
||||||
def new_image(
|
def new_image(
|
||||||
self,
|
self,
|
||||||
write_to_file: bool = True,
|
write_to_file: bool = True,
|
||||||
temporary: bool = True,
|
temporary: bool = True,
|
||||||
filename: str = None,
|
filename: str = None,
|
||||||
folder: str = "",
|
folder: str = "",
|
||||||
fmt: str = 'jpeg'):
|
fmt: str = "jpeg",
|
||||||
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create a new image capture object. Adds to the image list, and shunt all others.
|
Create a new image capture object. Adds to the image list, and shunt all others.
|
||||||
|
|
@ -275,7 +278,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
filename = "{}.{}".format(filename, fmt)
|
filename = "{}.{}".format(filename, fmt)
|
||||||
|
|
||||||
# Generate folder
|
# Generate folder
|
||||||
base_folder = self.paths['image_tmp'] if temporary else self.paths['image']
|
base_folder = self.paths["image_tmp"] if temporary else self.paths["image"]
|
||||||
folder = os.path.join(base_folder, folder)
|
folder = os.path.join(base_folder, folder)
|
||||||
|
|
||||||
# Generate file path
|
# Generate file path
|
||||||
|
|
@ -283,9 +286,8 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
# Create capture object
|
# Create capture object
|
||||||
output = CaptureObject(
|
output = CaptureObject(
|
||||||
write_to_file=write_to_file,
|
write_to_file=write_to_file, temporary=temporary, filepath=filepath
|
||||||
temporary=temporary,
|
)
|
||||||
filepath=filepath)
|
|
||||||
|
|
||||||
# Update capture list
|
# Update capture list
|
||||||
shunt_captures(self.images)
|
shunt_captures(self.images)
|
||||||
|
|
@ -294,12 +296,13 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def new_video(
|
def new_video(
|
||||||
self,
|
self,
|
||||||
write_to_file: bool = True,
|
write_to_file: bool = True,
|
||||||
temporary: bool = False,
|
temporary: bool = False,
|
||||||
filename: str = None,
|
filename: str = None,
|
||||||
folder: str = "",
|
folder: str = "",
|
||||||
fmt: str = 'h264'):
|
fmt: str = "h264",
|
||||||
|
):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create a new video capture object. Adds to the image list, and shunt all others.
|
Create a new video capture object. Adds to the image list, and shunt all others.
|
||||||
|
|
@ -320,7 +323,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
filename = "{}.{}".format(filename, fmt)
|
filename = "{}.{}".format(filename, fmt)
|
||||||
|
|
||||||
# Generate folder
|
# Generate folder
|
||||||
base_folder = self.paths['video_tmp'] if temporary else self.paths['video']
|
base_folder = self.paths["video_tmp"] if temporary else self.paths["video"]
|
||||||
folder = os.path.join(base_folder, folder)
|
folder = os.path.join(base_folder, folder)
|
||||||
|
|
||||||
# Generate file path
|
# Generate file path
|
||||||
|
|
@ -328,9 +331,8 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
# Create capture object
|
# Create capture object
|
||||||
output = CaptureObject(
|
output = CaptureObject(
|
||||||
write_to_file=write_to_file,
|
write_to_file=write_to_file, temporary=temporary, filepath=filepath
|
||||||
temporary=temporary,
|
)
|
||||||
filepath=filepath)
|
|
||||||
|
|
||||||
# Update capture list
|
# Update capture list
|
||||||
shunt_captures(self.videos)
|
shunt_captures(self.videos)
|
||||||
|
|
@ -345,7 +347,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.frames_iterator = self.frames()
|
self.frames_iterator = self.frames()
|
||||||
logging.debug("Entering worker thread.")
|
logging.debug("Entering worker thread.")
|
||||||
|
|
||||||
self.state['stream_active'] = True
|
self.state["stream_active"] = True
|
||||||
|
|
||||||
for frame in self.frames_iterator:
|
for frame in self.frames_iterator:
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
|
|
@ -354,9 +356,13 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
# Handle timeout
|
# Handle timeout
|
||||||
if (
|
if (
|
||||||
self.stream_timeout_enabled and # If using timeout
|
self.stream_timeout_enabled
|
||||||
(time.time() - self.last_access > self.stream_timeout) and # And timeout time
|
and ( # If using timeout
|
||||||
not self.state['preview_active'] # And GPU preview is not active
|
time.time() - self.last_access > self.stream_timeout
|
||||||
|
)
|
||||||
|
and not self.state[ # And timeout time
|
||||||
|
"preview_active"
|
||||||
|
] # And GPU preview is not active
|
||||||
):
|
):
|
||||||
self.frames_iterator.close()
|
self.frames_iterator.close()
|
||||||
break
|
break
|
||||||
|
|
@ -372,4 +378,4 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
logging.debug("BaseCamera worker thread exiting...")
|
logging.debug("BaseCamera worker thread exiting...")
|
||||||
# Set stream_activate state
|
# Set stream_activate state
|
||||||
self.state['stream_active'] = False
|
self.state["stream_active"] = False
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ Attributes:
|
||||||
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
|
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
|
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
|
||||||
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF']
|
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
|
||||||
THUMBNAIL_SIZE = (200, 150)
|
THUMBNAIL_SIZE = (200, 150)
|
||||||
|
|
||||||
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
|
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
|
||||||
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
|
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
|
||||||
|
|
||||||
|
|
||||||
# TODO: Move these methods to a camera utilities module?
|
# TODO: Move these methods to a camera utilities module?
|
||||||
|
|
@ -50,8 +50,8 @@ def pull_usercomment_dict(filepath):
|
||||||
except InvalidImageDataError:
|
except InvalidImageDataError:
|
||||||
logging.error("Invalid data at {}. Skipping.".format(filepath))
|
logging.error("Invalid data at {}. Skipping.".format(filepath))
|
||||||
return None
|
return None
|
||||||
if 'Exif' in exif_dict and 37510 in exif_dict['Exif']:
|
if "Exif" in exif_dict and 37510 in exif_dict["Exif"]:
|
||||||
return yaml.load(exif_dict['Exif'][37510].decode())
|
return yaml.load(exif_dict["Exif"][37510].decode())
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -59,7 +59,9 @@ def pull_usercomment_dict(filepath):
|
||||||
def make_file_list(directory, formats):
|
def make_file_list(directory, formats):
|
||||||
files = []
|
files = []
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
files.extend(glob.glob('{}/**/*.{}'.format(directory, fmt.lower()), recursive=True))
|
files.extend(
|
||||||
|
glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True)
|
||||||
|
)
|
||||||
|
|
||||||
logging.info("{} capture files found on disk".format(len(files)))
|
logging.info("{} capture files found on disk".format(len(files)))
|
||||||
|
|
||||||
|
|
@ -98,21 +100,19 @@ def capture_from_exif(path, exif_dict):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create a placeholder capture
|
# Create a placeholder capture
|
||||||
capture = CaptureObject(
|
capture = CaptureObject(filepath=path)
|
||||||
filepath=path
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build file path information
|
# Build file path information
|
||||||
capture.split_file_path(capture.file)
|
capture.split_file_path(capture.file)
|
||||||
|
|
||||||
# Populate capture parameters
|
# Populate capture parameters
|
||||||
capture.id = exif_dict['id']
|
capture.id = exif_dict["id"]
|
||||||
|
|
||||||
capture.timestring = exif_dict['time']
|
capture.timestring = exif_dict["time"]
|
||||||
capture.format = exif_dict['format']
|
capture.format = exif_dict["format"]
|
||||||
|
|
||||||
capture._metadata = exif_dict['custom']
|
capture._metadata = exif_dict["custom"]
|
||||||
capture.tags = exif_dict['tags']
|
capture.tags = exif_dict["tags"]
|
||||||
|
|
||||||
return capture
|
return capture
|
||||||
|
|
||||||
|
|
@ -138,10 +138,8 @@ class CaptureObject(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, write_to_file: bool = False, temporary: bool = False, filepath: str = ""
|
||||||
write_to_file: bool = False,
|
) -> None:
|
||||||
temporary: bool = False,
|
|
||||||
filepath: str = '') -> None:
|
|
||||||
"""Create a new StreamObject, to manage capture data."""
|
"""Create a new StreamObject, to manage capture data."""
|
||||||
|
|
||||||
# Store a nice ID
|
# Store a nice ID
|
||||||
|
|
@ -184,7 +182,9 @@ class CaptureObject(object):
|
||||||
"""Create StreamObject in context, to auto-clean disk data."""
|
"""Create StreamObject in context, to auto-clean disk data."""
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(
|
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(
|
||||||
self.id))
|
self.id
|
||||||
|
)
|
||||||
|
)
|
||||||
self.temporary = True # Flag file to be removed on close.
|
self.temporary = True # Flag file to be removed on close.
|
||||||
self.context_manager = True # Used in metadata
|
self.context_manager = True # Used in metadata
|
||||||
|
|
||||||
|
|
@ -208,7 +208,9 @@ class CaptureObject(object):
|
||||||
base_dir = TEMP_CAPTURE_PATH
|
base_dir = TEMP_CAPTURE_PATH
|
||||||
else:
|
else:
|
||||||
base_dir = BASE_CAPTURE_PATH
|
base_dir = BASE_CAPTURE_PATH
|
||||||
return os.path.join(base_dir, self.filename) # Full file name by joining given folder to given name
|
return os.path.join(
|
||||||
|
base_dir, self.filename
|
||||||
|
) # Full file name by joining given folder to given name
|
||||||
|
|
||||||
def split_file_path(self, filepath):
|
def split_file_path(self, filepath):
|
||||||
"""
|
"""
|
||||||
|
|
@ -221,7 +223,7 @@ class CaptureObject(object):
|
||||||
self.filefolder, self.filename = os.path.split(filepath)
|
self.filefolder, self.filename = os.path.split(filepath)
|
||||||
# Split the filename out from it's file extension
|
# Split the filename out from it's file extension
|
||||||
self.basename = os.path.splitext(self.filename)[0]
|
self.basename = os.path.splitext(self.filename)[0]
|
||||||
self.format = self.filename.split('.')[-1]
|
self.format = self.filename.split(".")[-1]
|
||||||
|
|
||||||
# Create folder and file
|
# Create folder and file
|
||||||
if not os.path.exists(self.filefolder):
|
if not os.path.exists(self.filefolder):
|
||||||
|
|
@ -298,7 +300,7 @@ class CaptureObject(object):
|
||||||
# Serialize metadata
|
# Serialize metadata
|
||||||
metadata_string = yaml.safe_dump(self.metadata)
|
metadata_string = yaml.safe_dump(self.metadata)
|
||||||
# Insert metadata into exif_dict
|
# Insert metadata into exif_dict
|
||||||
exif_dict['Exif'][piexif.ExifIFD.UserComment] = metadata_string.encode()
|
exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode()
|
||||||
# Convert new exif dict to exif bytes
|
# Convert new exif dict to exif bytes
|
||||||
exif_bytes = piexif.dump(exif_dict)
|
exif_bytes = piexif.dump(exif_dict)
|
||||||
# Insert exif into file
|
# Insert exif into file
|
||||||
|
|
@ -310,8 +312,14 @@ class CaptureObject(object):
|
||||||
Create basic metadata dictionary from basic capture data,
|
Create basic metadata dictionary from basic capture data,
|
||||||
and any added custom metadata and tags.
|
and any added custom metadata and tags.
|
||||||
"""
|
"""
|
||||||
d = {'id': self.id, 'filename': self.filename, 'time': self.timestring,
|
d = {
|
||||||
'format': self.format, 'tags': self.tags, 'custom': self._metadata}
|
"id": self.id,
|
||||||
|
"filename": self.filename,
|
||||||
|
"time": self.timestring,
|
||||||
|
"format": self.format,
|
||||||
|
"tags": self.tags,
|
||||||
|
"custom": self._metadata,
|
||||||
|
}
|
||||||
|
|
||||||
# Add custom metadata to dictionary
|
# Add custom metadata to dictionary
|
||||||
return d
|
return d
|
||||||
|
|
@ -339,19 +347,19 @@ class CaptureObject(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create basic state dictionary
|
# Create basic state dictionary
|
||||||
d = {'path': self.file, 'temporary': self.temporary, 'metadata': self.metadata}
|
d = {"path": self.file, "temporary": self.temporary, "metadata": self.metadata}
|
||||||
|
|
||||||
# Check bytestream
|
# Check bytestream
|
||||||
if self.stream_exists:
|
if self.stream_exists:
|
||||||
d['bytestream'] = True
|
d["bytestream"] = True
|
||||||
else:
|
else:
|
||||||
d['bytestream'] = False
|
d["bytestream"] = False
|
||||||
|
|
||||||
# Combined availability of data
|
# Combined availability of data
|
||||||
if self.exists:
|
if self.exists:
|
||||||
d['available'] = True
|
d["available"] = True
|
||||||
else:
|
else:
|
||||||
d['available'] = False
|
d["available"] = False
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
@ -373,7 +381,7 @@ class CaptureObject(object):
|
||||||
else: # If data bytestream is empty
|
else: # If data bytestream is empty
|
||||||
if self.file_exists: # If data file exists
|
if self.file_exists: # If data file exists
|
||||||
logging.info("Opening from file {}".format(self.file))
|
logging.info("Opening from file {}".format(self.file))
|
||||||
with open(self.file, 'rb') as f:
|
with open(self.file, "rb") as f:
|
||||||
d = io.BytesIO(f.read()) # Load bytes from file
|
d = io.BytesIO(f.read()) # Load bytes from file
|
||||||
d.seek(0) # Rewind loaded bytestream
|
d.seek(0) # Rewind loaded bytestream
|
||||||
# Create a copy of the bytestream bytes
|
# Create a copy of the bytestream bytes
|
||||||
|
|
@ -413,7 +421,7 @@ class CaptureObject(object):
|
||||||
def load_file(self) -> bool:
|
def load_file(self) -> bool:
|
||||||
"""Load data stored on disk to the in-memory bytestream."""
|
"""Load data stored on disk to the in-memory bytestream."""
|
||||||
if self.file_exists: # If data file exists
|
if self.file_exists: # If data file exists
|
||||||
with open(self.file, 'rb') as f:
|
with open(self.file, "rb") as f:
|
||||||
self.bytestream = io.BytesIO(f.read()) # Load bytes from file
|
self.bytestream = io.BytesIO(f.read()) # Load bytes from file
|
||||||
self.bytestream.seek(0) # Rewind data bytes again
|
self.bytestream.seek(0) # Rewind data bytes again
|
||||||
return True
|
return True
|
||||||
|
|
@ -423,7 +431,7 @@ class CaptureObject(object):
|
||||||
def save_file(self) -> bool:
|
def save_file(self) -> bool:
|
||||||
"""Write the StreamObjects bytestream to a file."""
|
"""Write the StreamObjects bytestream to a file."""
|
||||||
if self.stream_exists: # If there's a bytestream to save
|
if self.stream_exists: # If there's a bytestream to save
|
||||||
with open(self.file, 'ab') as f: # Load file as bytes
|
with open(self.file, "ab") as f: # Load file as bytes
|
||||||
logging.debug("Writing bytestream to file {}".format(self.file))
|
logging.debug("Writing bytestream to file {}".format(self.file))
|
||||||
f.seek(0, 0) # Seek to the start of the file
|
f.seek(0, 0) # Seek to the start of the file
|
||||||
f.write(self.binary) # Write data bytes to file
|
f.write(self.binary) # Write data bytes to file
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import picamera.array
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from .base import BaseCamera, CaptureObject
|
from .base import BaseCamera, CaptureObject
|
||||||
|
|
||||||
# Richard's fix gain
|
# Richard's fix gain
|
||||||
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||||
|
|
||||||
|
|
@ -43,30 +44,31 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class PiCameraStreamer(BaseCamera):
|
class PiCameraStreamer(BaseCamera):
|
||||||
"""Raspberry Pi camera implementation of PiCameraStreamer."""
|
"""Raspberry Pi camera implementation of PiCameraStreamer."""
|
||||||
|
|
||||||
picamera_settings_keys = [
|
picamera_settings_keys = [
|
||||||
'exposure_mode',
|
"exposure_mode",
|
||||||
'analog_gain',
|
"analog_gain",
|
||||||
'digital_gain',
|
"digital_gain",
|
||||||
'shutter_speed',
|
"shutter_speed",
|
||||||
'awb_gains',
|
"awb_gains",
|
||||||
'awb_mode',
|
"awb_mode",
|
||||||
'framerate',
|
"framerate",
|
||||||
'saturation',
|
"saturation",
|
||||||
'lens_shading_table'
|
"lens_shading_table",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Run BaseCamera init
|
# Run BaseCamera init
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
# Attach to Pi camera
|
# Attach to Pi camera
|
||||||
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object
|
self.camera = (
|
||||||
|
picamera.PiCamera()
|
||||||
|
) #: :py:class:`picamera.PiCamera`: Picamera object
|
||||||
|
|
||||||
# Store state of PiCameraStreamer
|
# Store state of PiCameraStreamer
|
||||||
self.state.update({
|
self.state.update(
|
||||||
'stream_active': False,
|
{"stream_active": False, "record_active": False, "preview_active": False}
|
||||||
'record_active': False,
|
)
|
||||||
'preview_active': False,
|
|
||||||
})
|
|
||||||
# Reset variable states
|
# Reset variable states
|
||||||
self.set_zoom(1.0)
|
self.set_zoom(1.0)
|
||||||
|
|
||||||
|
|
@ -101,11 +103,11 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conf_dict = {
|
conf_dict = {
|
||||||
'stream_resolution': self.stream_resolution,
|
"stream_resolution": self.stream_resolution,
|
||||||
'image_resolution': self.image_resolution,
|
"image_resolution": self.image_resolution,
|
||||||
'numpy_resolution': self.numpy_resolution,
|
"numpy_resolution": self.numpy_resolution,
|
||||||
'jpeg_quality': self.jpeg_quality,
|
"jpeg_quality": self.jpeg_quality,
|
||||||
'picamera_settings': {},
|
"picamera_settings": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# PiCamera parameters
|
# PiCamera parameters
|
||||||
|
|
@ -113,7 +115,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
try:
|
try:
|
||||||
value = getattr(self.camera, key)
|
value = getattr(self.camera, key)
|
||||||
logging.debug("Reading PiCamera().{}: {}".format(key, value))
|
logging.debug("Reading PiCamera().{}: {}".format(key, value))
|
||||||
conf_dict['picamera_settings'][key] = value
|
conf_dict["picamera_settings"][key] = value
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.debug("Unable to read PiCamera attribute {}".format(key))
|
logging.debug("Unable to read PiCamera attribute {}".format(key))
|
||||||
|
|
||||||
|
|
@ -138,21 +140,23 @@ class PiCameraStreamer(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to Picamera object
|
# Apply valid config params to Picamera object
|
||||||
if not self.state['record_active']: # If not recording a video
|
if not self.state["record_active"]: # If not recording a video
|
||||||
|
|
||||||
# Pause stream while changing settings
|
# Pause stream while changing settings
|
||||||
if self.state['stream_active']: # If stream is active
|
if self.state["stream_active"]: # If stream is active
|
||||||
logging.info("Pausing stream to update config.")
|
logging.info("Pausing stream to update config.")
|
||||||
self.stop_stream_recording() # Pause stream
|
self.stop_stream_recording() # Pause stream
|
||||||
paused_stream = True # Remember to unpause stream when done
|
paused_stream = True # Remember to unpause stream when done
|
||||||
|
|
||||||
# PiCamera parameters
|
# PiCamera parameters
|
||||||
if 'picamera_settings' in config: # If new settings are given
|
if "picamera_settings" in config: # If new settings are given
|
||||||
self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True)
|
self.apply_picamera_settings(
|
||||||
|
config["picamera_settings"], pause_for_effect=True
|
||||||
|
)
|
||||||
|
|
||||||
# PiCameraStreamer parameters
|
# PiCameraStreamer parameters
|
||||||
for key, value in config.items(): # For each provided setting
|
for key, value in config.items(): # For each provided setting
|
||||||
if (key != 'picamera_settings') and hasattr(self, key):
|
if (key != "picamera_settings") and hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
# If stream was paused to update config, unpause
|
# If stream was paused to update config, unpause
|
||||||
|
|
@ -162,67 +166,84 @@ class PiCameraStreamer(BaseCamera):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Cannot update camera config while recording is active.")
|
"Cannot update camera config while recording is active."
|
||||||
|
)
|
||||||
|
|
||||||
def apply_picamera_settings(self, settings_dict: dict, pause_for_effect: bool=True):
|
def apply_picamera_settings(
|
||||||
|
self, settings_dict: dict, pause_for_effect: bool = True
|
||||||
|
):
|
||||||
# Set exposure mode
|
# Set exposure mode
|
||||||
if 'exposure_mode' in settings_dict:
|
if "exposure_mode" in settings_dict:
|
||||||
logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode']))
|
logging.debug(
|
||||||
self.camera.exposure_mode = settings_dict['exposure_mode']
|
"Applying exposure_mode: {}".format(settings_dict["exposure_mode"])
|
||||||
|
)
|
||||||
|
self.camera.exposure_mode = settings_dict["exposure_mode"]
|
||||||
|
|
||||||
# Apply gains and let them settle
|
# Apply gains and let them settle
|
||||||
if 'analog_gain' in settings_dict:
|
if "analog_gain" in settings_dict:
|
||||||
logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain']))
|
logging.debug(
|
||||||
set_analog_gain(self.camera, float(settings_dict['analog_gain']))
|
"Applying analog_gain: {}".format(settings_dict["analog_gain"])
|
||||||
if 'digital_gain' in settings_dict:
|
)
|
||||||
logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain']))
|
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
|
||||||
set_digital_gain(self.camera, float(settings_dict['digital_gain']))
|
if "digital_gain" in settings_dict:
|
||||||
|
logging.debug(
|
||||||
|
"Applying digital_gain: {}".format(settings_dict["digital_gain"])
|
||||||
|
)
|
||||||
|
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
|
||||||
|
|
||||||
# Apply shutter speed
|
# Apply shutter speed
|
||||||
if 'shutter_speed' in settings_dict:
|
if "shutter_speed" in settings_dict:
|
||||||
logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed']))
|
logging.debug(
|
||||||
self.camera.shutter_speed = int(settings_dict['shutter_speed'])
|
"Applying shutter_speed: {}".format(settings_dict["shutter_speed"])
|
||||||
|
)
|
||||||
|
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
|
||||||
|
|
||||||
time.sleep(0.2) # Let gains settle
|
time.sleep(0.2) # Let gains settle
|
||||||
|
|
||||||
# Handle AWB in a half-smart way
|
# Handle AWB in a half-smart way
|
||||||
if 'awb_gains' in settings_dict:
|
if "awb_gains" in settings_dict:
|
||||||
logging.debug("Applying awb_mode: off")
|
logging.debug("Applying awb_mode: off")
|
||||||
self.camera.awb_mode = 'off'
|
self.camera.awb_mode = "off"
|
||||||
logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains']))
|
logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"]))
|
||||||
self.camera.awb_gains = settings_dict['awb_gains']
|
self.camera.awb_gains = settings_dict["awb_gains"]
|
||||||
elif 'awb_mode' in settings_dict:
|
elif "awb_mode" in settings_dict:
|
||||||
logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode']))
|
logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"]))
|
||||||
self.camera.awb_mode = settings_dict['awb_mode']
|
self.camera.awb_mode = settings_dict["awb_mode"]
|
||||||
|
|
||||||
# Handle some properties that can be quickly applied
|
# Handle some properties that can be quickly applied
|
||||||
batched_keys = ['framerate', 'saturation']
|
batched_keys = ["framerate", "saturation"]
|
||||||
for key in batched_keys:
|
for key in batched_keys:
|
||||||
if (key in settings_dict) and hasattr(self.camera, key):
|
if (key in settings_dict) and hasattr(self.camera, key):
|
||||||
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
||||||
setattr(self.camera, key, settings_dict[key])
|
setattr(self.camera, key, settings_dict[key])
|
||||||
|
|
||||||
# Handle lens shading if camera supports it
|
# Handle lens shading if camera supports it
|
||||||
if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'):
|
if ("lens_shading_table" in settings_dict) and hasattr(
|
||||||
logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table']))
|
self.camera, "lens_shading_table"
|
||||||
self.camera.lens_shading_table = settings_dict['lens_shading_table']
|
):
|
||||||
|
logging.debug(
|
||||||
|
"Applying lens_shading_table: {}".format(
|
||||||
|
settings_dict["lens_shading_table"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.camera.lens_shading_table = settings_dict["lens_shading_table"]
|
||||||
|
|
||||||
# Final optional pause to settle
|
# Final optional pause to settle
|
||||||
if pause_for_effect:
|
if pause_for_effect:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
def set_zoom(self, zoom_value: float = 1.) -> None:
|
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
||||||
"""
|
"""
|
||||||
Change the camera zoom, handling re-centering and scaling.
|
Change the camera zoom, handling re-centering and scaling.
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.state['zoom_value'] = float(zoom_value)
|
self.state["zoom_value"] = float(zoom_value)
|
||||||
if self.state['zoom_value'] < 1:
|
if self.state["zoom_value"] < 1:
|
||||||
self.state['zoom_value'] = 1
|
self.state["zoom_value"] = 1
|
||||||
# Richard's code for zooming !
|
# Richard's code for zooming !
|
||||||
fov = self.camera.zoom
|
fov = self.camera.zoom
|
||||||
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
||||||
size = 1.0 / self.state['zoom_value']
|
size = 1.0 / self.state["zoom_value"]
|
||||||
# If the new zoom value would be invalid, move the centre to
|
# If the new zoom value would be invalid, move the centre to
|
||||||
# keep it within the camera's sensor (this is only relevant
|
# keep it within the camera's sensor (this is only relevant
|
||||||
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
||||||
|
|
@ -252,22 +273,24 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.preview.window = window
|
self.camera.preview.window = window
|
||||||
if fullscreen:
|
if fullscreen:
|
||||||
self.camera.preview.fullscreen = fullscreen
|
self.camera.preview.fullscreen = fullscreen
|
||||||
self.state['preview_active'] = True
|
self.state["preview_active"] = True
|
||||||
except picamera.exc.PiCameraMMALError as e:
|
except picamera.exc.PiCameraMMALError as e:
|
||||||
logging.error("Suppressed a MMALError in start_preview. Exception: {}".format(e))
|
logging.error(
|
||||||
|
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
||||||
|
)
|
||||||
except picamera.exc.PiCameraValueError as e:
|
except picamera.exc.PiCameraValueError as e:
|
||||||
logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e))
|
logging.error(
|
||||||
|
"Suppressed a ValueError exception in start_preview. Exception: {}".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def stop_preview(self):
|
def stop_preview(self):
|
||||||
"""Stop the on board GPU camera preview."""
|
"""Stop the on board GPU camera preview."""
|
||||||
self.camera.stop_preview()
|
self.camera.stop_preview()
|
||||||
self.state['preview_active'] = False
|
self.state["preview_active"] = False
|
||||||
|
|
||||||
def start_recording(
|
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
|
||||||
self,
|
|
||||||
output,
|
|
||||||
fmt: str = 'h264',
|
|
||||||
quality: int = 15):
|
|
||||||
"""Start recording.
|
"""Start recording.
|
||||||
|
|
||||||
Start a new video recording, writing to a output object.
|
Start a new video recording, writing to a output object.
|
||||||
|
|
@ -283,7 +306,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Start recording method only if a current recording is not running
|
# Start recording method only if a current recording is not running
|
||||||
if not self.state['record_active']:
|
if not self.state["record_active"]:
|
||||||
|
|
||||||
# If output is a StreamObject
|
# If output is a StreamObject
|
||||||
if isinstance(output, CaptureObject):
|
if isinstance(output, CaptureObject):
|
||||||
|
|
@ -300,17 +323,19 @@ class PiCameraStreamer(BaseCamera):
|
||||||
format=fmt,
|
format=fmt,
|
||||||
splitter_port=2,
|
splitter_port=2,
|
||||||
resize=self.stream_resolution,
|
resize=self.stream_resolution,
|
||||||
quality=quality)
|
quality=quality,
|
||||||
|
)
|
||||||
|
|
||||||
# Update state dictionary
|
# Update state dictionary
|
||||||
self.state['record_active'] = True
|
self.state["record_active"] = True
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Cannot start a new recording\
|
"Cannot start a new recording\
|
||||||
until the current recording has stopped.")
|
until the current recording has stopped."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def stop_recording(self):
|
def stop_recording(self):
|
||||||
|
|
@ -322,12 +347,11 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.info("Recording stopped")
|
logging.info("Recording stopped")
|
||||||
|
|
||||||
# Update state dictionary
|
# Update state dictionary
|
||||||
self.state['record_active'] = False
|
self.state["record_active"] = False
|
||||||
|
|
||||||
def stop_stream_recording(
|
def stop_stream_recording(
|
||||||
self,
|
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
||||||
splitter_port: int = 1,
|
) -> None:
|
||||||
resolution: Tuple[int, int] = None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
|
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
|
||||||
|
|
||||||
|
|
@ -346,16 +370,21 @@ class PiCameraStreamer(BaseCamera):
|
||||||
except picamera.exc.PiCameraNotRecording:
|
except picamera.exc.PiCameraNotRecording:
|
||||||
logging.info("Not recording on splitter_port {}".format(splitter_port))
|
logging.info("Not recording on splitter_port {}".format(splitter_port))
|
||||||
else:
|
else:
|
||||||
logging.info("Stopped MJPEG stream on port {1}. Switching to {0}.".format(resolution, splitter_port))
|
logging.info(
|
||||||
|
"Stopped MJPEG stream on port {1}. Switching to {0}.".format(
|
||||||
|
resolution, splitter_port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Increase the resolution for taking an image
|
# Increase the resolution for taking an image
|
||||||
time.sleep(0.2) # Sprinkled a sleep to prevent camera getting confused by rapid commands
|
time.sleep(
|
||||||
|
0.2
|
||||||
|
) # Sprinkled a sleep to prevent camera getting confused by rapid commands
|
||||||
self.camera.resolution = resolution
|
self.camera.resolution = resolution
|
||||||
|
|
||||||
def start_stream_recording(
|
def start_stream_recording(
|
||||||
self,
|
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
||||||
splitter_port: int = 1,
|
) -> None:
|
||||||
resolution: Tuple[int, int] = None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
|
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
|
||||||
|
|
||||||
|
|
@ -366,45 +395,57 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# If stream object was destroyed
|
# If stream object was destroyed
|
||||||
if not hasattr(self, 'stream'):
|
if not hasattr(self, "stream"):
|
||||||
self.stream = io.BytesIO() # Create a stream object
|
self.stream = io.BytesIO() # Create a stream object
|
||||||
|
|
||||||
# If no explicit resolution is passed
|
# If no explicit resolution is passed
|
||||||
if not resolution:
|
if not resolution:
|
||||||
resolution = self.stream_resolution # Default to video recording resolution
|
resolution = (
|
||||||
|
self.stream_resolution
|
||||||
|
) # Default to video recording resolution
|
||||||
|
|
||||||
# Reduce the resolution for video streaming
|
# Reduce the resolution for video streaming
|
||||||
try:
|
try:
|
||||||
self.camera._check_recording_stopped()
|
self.camera._check_recording_stopped()
|
||||||
except picamera.exc.PiCameraRuntimeError:
|
except picamera.exc.PiCameraRuntimeError:
|
||||||
logging.info("Error while changing resolution: Recording already running.")
|
logging.info(
|
||||||
|
"Error while changing resolution: Recording already running."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.camera.resolution = resolution
|
self.camera.resolution = resolution
|
||||||
|
|
||||||
# If the stream should be active
|
# If the stream should be active
|
||||||
if self.state['stream_active']:
|
if self.state["stream_active"]:
|
||||||
try:
|
try:
|
||||||
# Start recording on stream port
|
# Start recording on stream port
|
||||||
self.camera.start_recording(
|
self.camera.start_recording(
|
||||||
self.stream,
|
self.stream,
|
||||||
format='mjpeg',
|
format="mjpeg",
|
||||||
quality=self.jpeg_quality,
|
quality=self.jpeg_quality,
|
||||||
bitrate=-1, # RWB: disable bitrate control
|
bitrate=-1, # RWB: disable bitrate control
|
||||||
# (bitrate control makes JPEG size less good as a focus
|
# (bitrate control makes JPEG size less good as a focus
|
||||||
# metric)
|
# metric)
|
||||||
splitter_port=splitter_port)
|
splitter_port=splitter_port,
|
||||||
|
)
|
||||||
except picamera.exc.PiCameraAlreadyRecording:
|
except picamera.exc.PiCameraAlreadyRecording:
|
||||||
logging.info("Error while starting preview: Recording already running.")
|
logging.info(
|
||||||
|
"Error while starting preview: Recording already running."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug("Started MJPEG stream at {} on port {}".format(resolution, splitter_port))
|
logging.debug(
|
||||||
|
"Started MJPEG stream at {} on port {}".format(
|
||||||
|
resolution, splitter_port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def capture(
|
def capture(
|
||||||
self,
|
self,
|
||||||
output,
|
output,
|
||||||
fmt: str = 'jpeg',
|
fmt: str = "jpeg",
|
||||||
use_video_port: bool = False,
|
use_video_port: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
resize: Tuple[int, int] = None,
|
||||||
bayer: bool = True):
|
bayer: bool = True,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Capture a still image to a StreamObject.
|
Capture a still image to a StreamObject.
|
||||||
|
|
||||||
|
|
@ -439,7 +480,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
quality=100,
|
quality=100,
|
||||||
resize=resize,
|
resize=resize,
|
||||||
bayer=(not use_video_port) and bayer,
|
bayer=(not use_video_port) and bayer,
|
||||||
use_video_port=use_video_port)
|
use_video_port=use_video_port,
|
||||||
|
)
|
||||||
|
|
||||||
# Set resolution and start stream recording if necessary
|
# Set resolution and start stream recording if necessary
|
||||||
if not use_video_port:
|
if not use_video_port:
|
||||||
|
|
@ -448,9 +490,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def yuv(
|
def yuv(
|
||||||
self,
|
self, use_video_port: bool = True, resize: Tuple[int, int] = None
|
||||||
use_video_port: bool = True,
|
) -> np.ndarray:
|
||||||
resize: Tuple[int, int] = None) -> np.ndarray:
|
|
||||||
"""Capture an uncompressed still YUV image to a Numpy array.
|
"""Capture an uncompressed still YUV image to a Numpy array.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -477,10 +518,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.info("Capturing to {}".format(output))
|
logging.info("Capturing to {}".format(output))
|
||||||
|
|
||||||
self.camera.capture(
|
self.camera.capture(
|
||||||
output,
|
output, resize=size, format="yuv", use_video_port=use_video_port
|
||||||
resize=size,
|
)
|
||||||
format='yuv',
|
|
||||||
use_video_port=use_video_port)
|
|
||||||
|
|
||||||
if not use_video_port:
|
if not use_video_port:
|
||||||
self.start_stream_recording()
|
self.start_stream_recording()
|
||||||
|
|
@ -488,9 +527,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
return output.array
|
return output.array
|
||||||
|
|
||||||
def array(
|
def array(
|
||||||
self,
|
self, use_video_port: bool = True, resize: Tuple[int, int] = None
|
||||||
use_video_port: bool = True,
|
) -> np.ndarray:
|
||||||
resize: Tuple[int, int] = None) -> np.ndarray:
|
|
||||||
"""Capture an uncompressed still RGB image to a Numpy array.
|
"""Capture an uncompressed still RGB image to a Numpy array.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -517,10 +555,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.info("Capturing to {}".format(output))
|
logging.info("Capturing to {}".format(output))
|
||||||
|
|
||||||
self.camera.capture(
|
self.camera.capture(
|
||||||
output,
|
output, resize=size, format="rgb", use_video_port=use_video_port
|
||||||
resize=size,
|
)
|
||||||
format='rgb',
|
|
||||||
use_video_port=use_video_port)
|
|
||||||
|
|
||||||
# Resume stream
|
# Resume stream
|
||||||
self.start_stream_recording()
|
self.start_stream_recording()
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,4 @@ from ._exif import *
|
||||||
from ._exceptions import *
|
from ._exceptions import *
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = "1.1.2"
|
||||||
VERSION = '1.1.2'
|
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,21 @@ def split_into_segments(data):
|
||||||
head = 2
|
head = 2
|
||||||
segments = [b"\xff\xd8"]
|
segments = [b"\xff\xd8"]
|
||||||
while 1:
|
while 1:
|
||||||
if data[head: head + 2] == b"\xff\xda":
|
if data[head : head + 2] == b"\xff\xda":
|
||||||
segments.append(data[head:])
|
segments.append(data[head:])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
length = struct.unpack(">H", data[head + 2: head + 4])[0]
|
length = struct.unpack(">H", data[head + 2 : head + 4])[0]
|
||||||
endPoint = head + length + 2
|
endPoint = head + length + 2
|
||||||
seg = data[head: endPoint]
|
seg = data[head:endPoint]
|
||||||
segments.append(seg)
|
segments.append(seg)
|
||||||
head = endPoint
|
head = endPoint
|
||||||
|
|
||||||
if (head >= len(data)):
|
if head >= len(data):
|
||||||
raise InvalidImageDataError("Wrong JPEG data.")
|
raise InvalidImageDataError("Wrong JPEG data.")
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
|
|
||||||
def read_exif_from_file(filename):
|
def read_exif_from_file(filename):
|
||||||
"""Slices JPEG meta data into a list from JPEG binary data.
|
"""Slices JPEG meta data into a list from JPEG binary data.
|
||||||
"""
|
"""
|
||||||
|
|
@ -39,11 +40,11 @@ def read_exif_from_file(filename):
|
||||||
HEAD_LENGTH = 4
|
HEAD_LENGTH = 4
|
||||||
exif = None
|
exif = None
|
||||||
while 1:
|
while 1:
|
||||||
length = struct.unpack(">H", head[2: 4])[0]
|
length = struct.unpack(">H", head[2:4])[0]
|
||||||
|
|
||||||
if head[:2] == b"\xff\xe1":
|
if head[:2] == b"\xff\xe1":
|
||||||
segment_data = f.read(length - 2)
|
segment_data = f.read(length - 2)
|
||||||
if segment_data[:4] != b'Exif':
|
if segment_data[:4] != b"Exif":
|
||||||
head = f.read(HEAD_LENGTH)
|
head = f.read(HEAD_LENGTH)
|
||||||
continue
|
continue
|
||||||
exif = head + segment_data
|
exif = head + segment_data
|
||||||
|
|
@ -57,6 +58,7 @@ def read_exif_from_file(filename):
|
||||||
f.close()
|
f.close()
|
||||||
return exif
|
return exif
|
||||||
|
|
||||||
|
|
||||||
def get_exif_seg(segments):
|
def get_exif_seg(segments):
|
||||||
"""Returns Exif from JPEG meta data list
|
"""Returns Exif from JPEG meta data list
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,9 +71,11 @@ def get_exif_seg(segments):
|
||||||
def merge_segments(segments, exif=b""):
|
def merge_segments(segments, exif=b""):
|
||||||
"""Merges Exif with APP0 and APP1 manipulations.
|
"""Merges Exif with APP0 and APP1 manipulations.
|
||||||
"""
|
"""
|
||||||
if segments[1][0:2] == b"\xff\xe0" and \
|
if (
|
||||||
segments[2][0:2] == b"\xff\xe1" and \
|
segments[1][0:2] == b"\xff\xe0"
|
||||||
segments[2][4:10] == b"Exif\x00\x00":
|
and segments[2][0:2] == b"\xff\xe1"
|
||||||
|
and segments[2][4:10] == b"Exif\x00\x00"
|
||||||
|
):
|
||||||
if exif:
|
if exif:
|
||||||
segments[2] = exif
|
segments[2] = exif
|
||||||
segments.pop(1)
|
segments.pop(1)
|
||||||
|
|
@ -82,8 +86,7 @@ def merge_segments(segments, exif=b""):
|
||||||
elif segments[1][0:2] == b"\xff\xe0":
|
elif segments[1][0:2] == b"\xff\xe0":
|
||||||
if exif:
|
if exif:
|
||||||
segments[1] = exif
|
segments[1] = exif
|
||||||
elif segments[1][0:2] == b"\xff\xe1" and \
|
elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00":
|
||||||
segments[1][4:10] == b"Exif\x00\x00":
|
|
||||||
if exif:
|
if exif:
|
||||||
segments[1] = exif
|
segments[1] = exif
|
||||||
elif exif is None:
|
elif exif is None:
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,20 @@ def dump(exif_dict_original):
|
||||||
else:
|
else:
|
||||||
zeroth_ifd = {}
|
zeroth_ifd = {}
|
||||||
|
|
||||||
if (("Exif" in exif_dict) and len(exif_dict["Exif"]) or
|
if (
|
||||||
("Interop" in exif_dict) and len(exif_dict["Interop"]) ):
|
("Exif" in exif_dict)
|
||||||
|
and len(exif_dict["Exif"])
|
||||||
|
or ("Interop" in exif_dict)
|
||||||
|
and len(exif_dict["Interop"])
|
||||||
|
):
|
||||||
zeroth_ifd[ImageIFD.ExifTag] = 1
|
zeroth_ifd[ImageIFD.ExifTag] = 1
|
||||||
exif_is = True
|
exif_is = True
|
||||||
exif_ifd = exif_dict["Exif"]
|
exif_ifd = exif_dict["Exif"]
|
||||||
if ("Interop" in exif_dict) and len(exif_dict["Interop"]):
|
if ("Interop" in exif_dict) and len(exif_dict["Interop"]):
|
||||||
exif_ifd[ExifIFD. InteroperabilityTag] = 1
|
exif_ifd[ExifIFD.InteroperabilityTag] = 1
|
||||||
interop_is = True
|
interop_is = True
|
||||||
interop_ifd = exif_dict["Interop"]
|
interop_ifd = exif_dict["Interop"]
|
||||||
elif ExifIFD. InteroperabilityTag in exif_ifd:
|
elif ExifIFD.InteroperabilityTag in exif_ifd:
|
||||||
exif_ifd.pop(ExifIFD.InteroperabilityTag)
|
exif_ifd.pop(ExifIFD.InteroperabilityTag)
|
||||||
elif ImageIFD.ExifTag in zeroth_ifd:
|
elif ImageIFD.ExifTag in zeroth_ifd:
|
||||||
zeroth_ifd.pop(ImageIFD.ExifTag)
|
zeroth_ifd.pop(ImageIFD.ExifTag)
|
||||||
|
|
@ -52,17 +56,20 @@ def dump(exif_dict_original):
|
||||||
elif ImageIFD.GPSTag in zeroth_ifd:
|
elif ImageIFD.GPSTag in zeroth_ifd:
|
||||||
zeroth_ifd.pop(ImageIFD.GPSTag)
|
zeroth_ifd.pop(ImageIFD.GPSTag)
|
||||||
|
|
||||||
if (("1st" in exif_dict) and
|
if (
|
||||||
("thumbnail" in exif_dict) and
|
("1st" in exif_dict)
|
||||||
(exif_dict["thumbnail"] is not None)):
|
and ("thumbnail" in exif_dict)
|
||||||
|
and (exif_dict["thumbnail"] is not None)
|
||||||
|
):
|
||||||
first_is = True
|
first_is = True
|
||||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1
|
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1
|
||||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1
|
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1
|
||||||
first_ifd = exif_dict["1st"]
|
first_ifd = exif_dict["1st"]
|
||||||
|
|
||||||
zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0)
|
zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0)
|
||||||
zeroth_length = (len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 +
|
zeroth_length = (
|
||||||
len(zeroth_set[1]))
|
len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1])
|
||||||
|
)
|
||||||
|
|
||||||
if exif_is:
|
if exif_is:
|
||||||
exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length)
|
exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length)
|
||||||
|
|
@ -115,8 +122,7 @@ def dump(exif_dict_original):
|
||||||
else:
|
else:
|
||||||
gps_pointer = b""
|
gps_pointer = b""
|
||||||
if interop_is:
|
if interop_is:
|
||||||
pointer_value = (TIFF_HEADER_LENGTH +
|
pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length
|
||||||
zeroth_length + exif_length + gps_length)
|
|
||||||
pointer_str = struct.pack(">I", pointer_value)
|
pointer_str = struct.pack(">I", pointer_value)
|
||||||
key = ExifIFD.InteroperabilityTag
|
key = ExifIFD.InteroperabilityTag
|
||||||
key_str = struct.pack(">H", key)
|
key_str = struct.pack(">H", key)
|
||||||
|
|
@ -126,33 +132,46 @@ def dump(exif_dict_original):
|
||||||
else:
|
else:
|
||||||
interop_pointer = b""
|
interop_pointer = b""
|
||||||
if first_is:
|
if first_is:
|
||||||
pointer_value = (TIFF_HEADER_LENGTH + zeroth_length +
|
pointer_value = (
|
||||||
exif_length + gps_length + interop_length)
|
TIFF_HEADER_LENGTH
|
||||||
|
+ zeroth_length
|
||||||
|
+ exif_length
|
||||||
|
+ gps_length
|
||||||
|
+ interop_length
|
||||||
|
)
|
||||||
first_ifd_pointer = struct.pack(">L", pointer_value)
|
first_ifd_pointer = struct.pack(">L", pointer_value)
|
||||||
thumbnail_pointer = (pointer_value + len(first_set[0]) + 24 +
|
thumbnail_pointer = (
|
||||||
4 + len(first_set[1]))
|
pointer_value + len(first_set[0]) + 24 + 4 + len(first_set[1])
|
||||||
thumbnail_p_bytes = (b"\x02\x01\x00\x04\x00\x00\x00\x01" +
|
)
|
||||||
struct.pack(">L", thumbnail_pointer))
|
thumbnail_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack(
|
||||||
thumbnail_length_bytes = (b"\x02\x02\x00\x04\x00\x00\x00\x01" +
|
">L", thumbnail_pointer
|
||||||
struct.pack(">L", len(thumbnail)))
|
)
|
||||||
first_bytes = (first_set[0] + thumbnail_p_bytes +
|
thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack(
|
||||||
thumbnail_length_bytes + b"\x00\x00\x00\x00" +
|
">L", len(thumbnail)
|
||||||
first_set[1] + thumbnail)
|
)
|
||||||
|
first_bytes = (
|
||||||
|
first_set[0]
|
||||||
|
+ thumbnail_p_bytes
|
||||||
|
+ thumbnail_length_bytes
|
||||||
|
+ b"\x00\x00\x00\x00"
|
||||||
|
+ first_set[1]
|
||||||
|
+ thumbnail
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
first_ifd_pointer = b"\x00\x00\x00\x00"
|
first_ifd_pointer = b"\x00\x00\x00\x00"
|
||||||
|
|
||||||
zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer +
|
zeroth_bytes = (
|
||||||
first_ifd_pointer + zeroth_set[1])
|
zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1]
|
||||||
|
)
|
||||||
if exif_is:
|
if exif_is:
|
||||||
exif_bytes = exif_set[0] + interop_pointer + exif_set[1]
|
exif_bytes = exif_set[0] + interop_pointer + exif_set[1]
|
||||||
|
|
||||||
return (header + zeroth_bytes + exif_bytes + gps_bytes +
|
return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes
|
||||||
interop_bytes + first_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_thumbnail(jpeg):
|
def _get_thumbnail(jpeg):
|
||||||
segments = split_into_segments(jpeg)
|
segments = split_into_segments(jpeg)
|
||||||
while (b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef"):
|
while b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef":
|
||||||
segments.pop(1)
|
segments.pop(1)
|
||||||
thumbnail = b"".join(segments)
|
thumbnail = b"".join(segments)
|
||||||
return thumbnail
|
return thumbnail
|
||||||
|
|
@ -161,24 +180,31 @@ def _get_thumbnail(jpeg):
|
||||||
def _pack_byte(*args):
|
def _pack_byte(*args):
|
||||||
return struct.pack("B" * len(args), *args)
|
return struct.pack("B" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_signed_byte(*args):
|
def _pack_signed_byte(*args):
|
||||||
return struct.pack("b" * len(args), *args)
|
return struct.pack("b" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_short(*args):
|
def _pack_short(*args):
|
||||||
return struct.pack(">" + "H" * len(args), *args)
|
return struct.pack(">" + "H" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_signed_short(*args):
|
def _pack_signed_short(*args):
|
||||||
return struct.pack(">" + "h" * len(args), *args)
|
return struct.pack(">" + "h" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_long(*args):
|
def _pack_long(*args):
|
||||||
return struct.pack(">" + "L" * len(args), *args)
|
return struct.pack(">" + "L" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_slong(*args):
|
def _pack_slong(*args):
|
||||||
return struct.pack(">" + "l" * len(args), *args)
|
return struct.pack(">" + "l" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_float(*args):
|
def _pack_float(*args):
|
||||||
return struct.pack(">" + "f" * len(args), *args)
|
return struct.pack(">" + "f" * len(args), *args)
|
||||||
|
|
||||||
|
|
||||||
def _pack_double(*args):
|
def _pack_double(*args):
|
||||||
return struct.pack(">" + "d" * len(args), *args)
|
return struct.pack(">" + "d" * len(args), *args)
|
||||||
|
|
||||||
|
|
@ -190,16 +216,14 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
if value_type == TYPES.Byte:
|
if value_type == TYPES.Byte:
|
||||||
length = len(raw_value)
|
length = len(raw_value)
|
||||||
if length <= 4:
|
if length <= 4:
|
||||||
value_str = (_pack_byte(*raw_value) +
|
value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length)
|
||||||
b"\x00" * (4 - length))
|
|
||||||
else:
|
else:
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_byte(*raw_value)
|
four_bytes_over = _pack_byte(*raw_value)
|
||||||
elif value_type == TYPES.Short:
|
elif value_type == TYPES.Short:
|
||||||
length = len(raw_value)
|
length = len(raw_value)
|
||||||
if length <= 2:
|
if length <= 2:
|
||||||
value_str = (_pack_short(*raw_value) +
|
value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length)
|
||||||
b"\x00\x00" * (2 - length))
|
|
||||||
else:
|
else:
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_short(*raw_value)
|
four_bytes_over = _pack_short(*raw_value)
|
||||||
|
|
@ -241,8 +265,7 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
new_value = b""
|
new_value = b""
|
||||||
for n, val in enumerate(raw_value):
|
for n, val in enumerate(raw_value):
|
||||||
num, den = val
|
num, den = val
|
||||||
new_value += (struct.pack(">L", num) +
|
new_value += struct.pack(">L", num) + struct.pack(">L", den)
|
||||||
struct.pack(">L", den))
|
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = new_value
|
four_bytes_over = new_value
|
||||||
elif value_type == TYPES.SRational:
|
elif value_type == TYPES.SRational:
|
||||||
|
|
@ -255,8 +278,7 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
new_value = b""
|
new_value = b""
|
||||||
for n, val in enumerate(raw_value):
|
for n, val in enumerate(raw_value):
|
||||||
num, den = val
|
num, den = val
|
||||||
new_value += (struct.pack(">l", num) +
|
new_value += struct.pack(">l", num) + struct.pack(">l", den)
|
||||||
struct.pack(">l", den))
|
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = new_value
|
four_bytes_over = new_value
|
||||||
elif value_type == TYPES.Undefined:
|
elif value_type == TYPES.Undefined:
|
||||||
|
|
@ -272,19 +294,17 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
value_str = raw_value + b"\x00" * (4 - length)
|
value_str = raw_value + b"\x00" * (4 - length)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise ValueError("Got invalid type to convert.")
|
raise ValueError("Got invalid type to convert.")
|
||||||
elif value_type == TYPES.SByte: # Signed Byte
|
elif value_type == TYPES.SByte: # Signed Byte
|
||||||
length = len(raw_value)
|
length = len(raw_value)
|
||||||
if length <= 4:
|
if length <= 4:
|
||||||
value_str = (_pack_signed_byte(*raw_value) +
|
value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length)
|
||||||
b"\x00" * (4 - length))
|
|
||||||
else:
|
else:
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_signed_byte(*raw_value)
|
four_bytes_over = _pack_signed_byte(*raw_value)
|
||||||
elif value_type == TYPES.SShort: # Signed Short
|
elif value_type == TYPES.SShort: # Signed Short
|
||||||
length = len(raw_value)
|
length = len(raw_value)
|
||||||
if length <= 2:
|
if length <= 2:
|
||||||
value_str = (_pack_signed_short(*raw_value) +
|
value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length)
|
||||||
b"\x00\x00" * (2 - length))
|
|
||||||
else:
|
else:
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_signed_short(*raw_value)
|
four_bytes_over = _pack_signed_short(*raw_value)
|
||||||
|
|
@ -295,7 +315,7 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
else:
|
else:
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_float(*raw_value)
|
four_bytes_over = _pack_float(*raw_value)
|
||||||
elif value_type == TYPES.DFloat: # Double
|
elif value_type == TYPES.DFloat: # Double
|
||||||
length = len(raw_value)
|
length = len(raw_value)
|
||||||
value_str = struct.pack(">I", offset)
|
value_str = struct.pack(">I", offset)
|
||||||
four_bytes_over = _pack_double(*raw_value)
|
four_bytes_over = _pack_double(*raw_value)
|
||||||
|
|
@ -303,6 +323,7 @@ def _value_to_bytes(raw_value, value_type, offset):
|
||||||
length_str = struct.pack(">I", length)
|
length_str = struct.pack(">I", length)
|
||||||
return length_str, value_str, four_bytes_over
|
return length_str, value_str, four_bytes_over
|
||||||
|
|
||||||
|
|
||||||
def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
|
def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
|
||||||
tag_count = len(ifd_dict)
|
tag_count = len(ifd_dict)
|
||||||
entry_header = struct.pack(">H", tag_count)
|
entry_header = struct.pack(">H", tag_count)
|
||||||
|
|
@ -318,7 +339,10 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
|
||||||
continue
|
continue
|
||||||
elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag):
|
elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag):
|
||||||
continue
|
continue
|
||||||
elif (ifd == "1st") and (key in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)):
|
elif (ifd == "1st") and (
|
||||||
|
key
|
||||||
|
in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_value = ifd_dict[key]
|
raw_value = ifd_dict[key]
|
||||||
|
|
@ -332,13 +356,13 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
|
||||||
offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values)
|
offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
length_str, value_str, four_bytes_over = _value_to_bytes(raw_value,
|
length_str, value_str, four_bytes_over = _value_to_bytes(
|
||||||
value_type,
|
raw_value, value_type, offset
|
||||||
offset)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'"dump" got wrong type of exif value.\n' +
|
'"dump" got wrong type of exif value.\n'
|
||||||
'{0} in {1} IFD. Got as {2}.'.format(key, ifd, type(ifd_dict[key]))
|
+ "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key]))
|
||||||
)
|
)
|
||||||
|
|
||||||
entries += key_str + type_str + length_str + value_str
|
entries += key_str + type_str + length_str + value_str
|
||||||
|
|
|
||||||
|
|
@ -14,314 +14,322 @@ class TYPES:
|
||||||
|
|
||||||
|
|
||||||
TAGS = {
|
TAGS = {
|
||||||
'Image': {11: {'name': 'ProcessingSoftware', 'type': TYPES.Ascii},
|
"Image": {
|
||||||
254: {'name': 'NewSubfileType', 'type': TYPES.Long},
|
11: {"name": "ProcessingSoftware", "type": TYPES.Ascii},
|
||||||
255: {'name': 'SubfileType', 'type': TYPES.Short},
|
254: {"name": "NewSubfileType", "type": TYPES.Long},
|
||||||
256: {'name': 'ImageWidth', 'type': TYPES.Long},
|
255: {"name": "SubfileType", "type": TYPES.Short},
|
||||||
257: {'name': 'ImageLength', 'type': TYPES.Long},
|
256: {"name": "ImageWidth", "type": TYPES.Long},
|
||||||
258: {'name': 'BitsPerSample', 'type': TYPES.Short},
|
257: {"name": "ImageLength", "type": TYPES.Long},
|
||||||
259: {'name': 'Compression', 'type': TYPES.Short},
|
258: {"name": "BitsPerSample", "type": TYPES.Short},
|
||||||
262: {'name': 'PhotometricInterpretation', 'type': TYPES.Short},
|
259: {"name": "Compression", "type": TYPES.Short},
|
||||||
263: {'name': 'Threshholding', 'type': TYPES.Short},
|
262: {"name": "PhotometricInterpretation", "type": TYPES.Short},
|
||||||
264: {'name': 'CellWidth', 'type': TYPES.Short},
|
263: {"name": "Threshholding", "type": TYPES.Short},
|
||||||
265: {'name': 'CellLength', 'type': TYPES.Short},
|
264: {"name": "CellWidth", "type": TYPES.Short},
|
||||||
266: {'name': 'FillOrder', 'type': TYPES.Short},
|
265: {"name": "CellLength", "type": TYPES.Short},
|
||||||
269: {'name': 'DocumentName', 'type': TYPES.Ascii},
|
266: {"name": "FillOrder", "type": TYPES.Short},
|
||||||
270: {'name': 'ImageDescription', 'type': TYPES.Ascii},
|
269: {"name": "DocumentName", "type": TYPES.Ascii},
|
||||||
271: {'name': 'Make', 'type': TYPES.Ascii},
|
270: {"name": "ImageDescription", "type": TYPES.Ascii},
|
||||||
272: {'name': 'Model', 'type': TYPES.Ascii},
|
271: {"name": "Make", "type": TYPES.Ascii},
|
||||||
273: {'name': 'StripOffsets', 'type': TYPES.Long},
|
272: {"name": "Model", "type": TYPES.Ascii},
|
||||||
274: {'name': 'Orientation', 'type': TYPES.Short},
|
273: {"name": "StripOffsets", "type": TYPES.Long},
|
||||||
277: {'name': 'SamplesPerPixel', 'type': TYPES.Short},
|
274: {"name": "Orientation", "type": TYPES.Short},
|
||||||
278: {'name': 'RowsPerStrip', 'type': TYPES.Long},
|
277: {"name": "SamplesPerPixel", "type": TYPES.Short},
|
||||||
279: {'name': 'StripByteCounts', 'type': TYPES.Long},
|
278: {"name": "RowsPerStrip", "type": TYPES.Long},
|
||||||
282: {'name': 'XResolution', 'type': TYPES.Rational},
|
279: {"name": "StripByteCounts", "type": TYPES.Long},
|
||||||
283: {'name': 'YResolution', 'type': TYPES.Rational},
|
282: {"name": "XResolution", "type": TYPES.Rational},
|
||||||
284: {'name': 'PlanarConfiguration', 'type': TYPES.Short},
|
283: {"name": "YResolution", "type": TYPES.Rational},
|
||||||
290: {'name': 'GrayResponseUnit', 'type': TYPES.Short},
|
284: {"name": "PlanarConfiguration", "type": TYPES.Short},
|
||||||
291: {'name': 'GrayResponseCurve', 'type': TYPES.Short},
|
290: {"name": "GrayResponseUnit", "type": TYPES.Short},
|
||||||
292: {'name': 'T4Options', 'type': TYPES.Long},
|
291: {"name": "GrayResponseCurve", "type": TYPES.Short},
|
||||||
293: {'name': 'T6Options', 'type': TYPES.Long},
|
292: {"name": "T4Options", "type": TYPES.Long},
|
||||||
296: {'name': 'ResolutionUnit', 'type': TYPES.Short},
|
293: {"name": "T6Options", "type": TYPES.Long},
|
||||||
301: {'name': 'TransferFunction', 'type': TYPES.Short},
|
296: {"name": "ResolutionUnit", "type": TYPES.Short},
|
||||||
305: {'name': 'Software', 'type': TYPES.Ascii},
|
301: {"name": "TransferFunction", "type": TYPES.Short},
|
||||||
306: {'name': 'DateTime', 'type': TYPES.Ascii},
|
305: {"name": "Software", "type": TYPES.Ascii},
|
||||||
315: {'name': 'Artist', 'type': TYPES.Ascii},
|
306: {"name": "DateTime", "type": TYPES.Ascii},
|
||||||
316: {'name': 'HostComputer', 'type': TYPES.Ascii},
|
315: {"name": "Artist", "type": TYPES.Ascii},
|
||||||
317: {'name': 'Predictor', 'type': TYPES.Short},
|
316: {"name": "HostComputer", "type": TYPES.Ascii},
|
||||||
318: {'name': 'WhitePoint', 'type': TYPES.Rational},
|
317: {"name": "Predictor", "type": TYPES.Short},
|
||||||
319: {'name': 'PrimaryChromaticities', 'type': TYPES.Rational},
|
318: {"name": "WhitePoint", "type": TYPES.Rational},
|
||||||
320: {'name': 'ColorMap', 'type': TYPES.Short},
|
319: {"name": "PrimaryChromaticities", "type": TYPES.Rational},
|
||||||
321: {'name': 'HalftoneHints', 'type': TYPES.Short},
|
320: {"name": "ColorMap", "type": TYPES.Short},
|
||||||
322: {'name': 'TileWidth', 'type': TYPES.Short},
|
321: {"name": "HalftoneHints", "type": TYPES.Short},
|
||||||
323: {'name': 'TileLength', 'type': TYPES.Short},
|
322: {"name": "TileWidth", "type": TYPES.Short},
|
||||||
324: {'name': 'TileOffsets', 'type': TYPES.Short},
|
323: {"name": "TileLength", "type": TYPES.Short},
|
||||||
325: {'name': 'TileByteCounts', 'type': TYPES.Short},
|
324: {"name": "TileOffsets", "type": TYPES.Short},
|
||||||
330: {'name': 'SubIFDs', 'type': TYPES.Long},
|
325: {"name": "TileByteCounts", "type": TYPES.Short},
|
||||||
332: {'name': 'InkSet', 'type': TYPES.Short},
|
330: {"name": "SubIFDs", "type": TYPES.Long},
|
||||||
333: {'name': 'InkNames', 'type': TYPES.Ascii},
|
332: {"name": "InkSet", "type": TYPES.Short},
|
||||||
334: {'name': 'NumberOfInks', 'type': TYPES.Short},
|
333: {"name": "InkNames", "type": TYPES.Ascii},
|
||||||
336: {'name': 'DotRange', 'type': TYPES.Byte},
|
334: {"name": "NumberOfInks", "type": TYPES.Short},
|
||||||
337: {'name': 'TargetPrinter', 'type': TYPES.Ascii},
|
336: {"name": "DotRange", "type": TYPES.Byte},
|
||||||
338: {'name': 'ExtraSamples', 'type': TYPES.Short},
|
337: {"name": "TargetPrinter", "type": TYPES.Ascii},
|
||||||
339: {'name': 'SampleFormat', 'type': TYPES.Short},
|
338: {"name": "ExtraSamples", "type": TYPES.Short},
|
||||||
340: {'name': 'SMinSampleValue', 'type': TYPES.Short},
|
339: {"name": "SampleFormat", "type": TYPES.Short},
|
||||||
341: {'name': 'SMaxSampleValue', 'type': TYPES.Short},
|
340: {"name": "SMinSampleValue", "type": TYPES.Short},
|
||||||
342: {'name': 'TransferRange', 'type': TYPES.Short},
|
341: {"name": "SMaxSampleValue", "type": TYPES.Short},
|
||||||
343: {'name': 'ClipPath', 'type': TYPES.Byte},
|
342: {"name": "TransferRange", "type": TYPES.Short},
|
||||||
344: {'name': 'XClipPathUnits', 'type': TYPES.Long},
|
343: {"name": "ClipPath", "type": TYPES.Byte},
|
||||||
345: {'name': 'YClipPathUnits', 'type': TYPES.Long},
|
344: {"name": "XClipPathUnits", "type": TYPES.Long},
|
||||||
346: {'name': 'Indexed', 'type': TYPES.Short},
|
345: {"name": "YClipPathUnits", "type": TYPES.Long},
|
||||||
347: {'name': 'JPEGTables', 'type': TYPES.Undefined},
|
346: {"name": "Indexed", "type": TYPES.Short},
|
||||||
351: {'name': 'OPIProxy', 'type': TYPES.Short},
|
347: {"name": "JPEGTables", "type": TYPES.Undefined},
|
||||||
512: {'name': 'JPEGProc', 'type': TYPES.Long},
|
351: {"name": "OPIProxy", "type": TYPES.Short},
|
||||||
513: {'name': 'JPEGInterchangeFormat', 'type': TYPES.Long},
|
512: {"name": "JPEGProc", "type": TYPES.Long},
|
||||||
514: {'name': 'JPEGInterchangeFormatLength', 'type': TYPES.Long},
|
513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long},
|
||||||
515: {'name': 'JPEGRestartInterval', 'type': TYPES.Short},
|
514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long},
|
||||||
517: {'name': 'JPEGLosslessPredictors', 'type': TYPES.Short},
|
515: {"name": "JPEGRestartInterval", "type": TYPES.Short},
|
||||||
518: {'name': 'JPEGPointTransforms', 'type': TYPES.Short},
|
517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short},
|
||||||
519: {'name': 'JPEGQTables', 'type': TYPES.Long},
|
518: {"name": "JPEGPointTransforms", "type": TYPES.Short},
|
||||||
520: {'name': 'JPEGDCTables', 'type': TYPES.Long},
|
519: {"name": "JPEGQTables", "type": TYPES.Long},
|
||||||
521: {'name': 'JPEGACTables', 'type': TYPES.Long},
|
520: {"name": "JPEGDCTables", "type": TYPES.Long},
|
||||||
529: {'name': 'YCbCrCoefficients', 'type': TYPES.Rational},
|
521: {"name": "JPEGACTables", "type": TYPES.Long},
|
||||||
530: {'name': 'YCbCrSubSampling', 'type': TYPES.Short},
|
529: {"name": "YCbCrCoefficients", "type": TYPES.Rational},
|
||||||
531: {'name': 'YCbCrPositioning', 'type': TYPES.Short},
|
530: {"name": "YCbCrSubSampling", "type": TYPES.Short},
|
||||||
532: {'name': 'ReferenceBlackWhite', 'type': TYPES.Rational},
|
531: {"name": "YCbCrPositioning", "type": TYPES.Short},
|
||||||
700: {'name': 'XMLPacket', 'type': TYPES.Byte},
|
532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational},
|
||||||
18246: {'name': 'Rating', 'type': TYPES.Short},
|
700: {"name": "XMLPacket", "type": TYPES.Byte},
|
||||||
18249: {'name': 'RatingPercent', 'type': TYPES.Short},
|
18246: {"name": "Rating", "type": TYPES.Short},
|
||||||
32781: {'name': 'ImageID', 'type': TYPES.Ascii},
|
18249: {"name": "RatingPercent", "type": TYPES.Short},
|
||||||
33421: {'name': 'CFARepeatPatternDim', 'type': TYPES.Short},
|
32781: {"name": "ImageID", "type": TYPES.Ascii},
|
||||||
33422: {'name': 'CFAPattern', 'type': TYPES.Byte},
|
33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short},
|
||||||
33423: {'name': 'BatteryLevel', 'type': TYPES.Rational},
|
33422: {"name": "CFAPattern", "type": TYPES.Byte},
|
||||||
33432: {'name': 'Copyright', 'type': TYPES.Ascii},
|
33423: {"name": "BatteryLevel", "type": TYPES.Rational},
|
||||||
33434: {'name': 'ExposureTime', 'type': TYPES.Rational},
|
33432: {"name": "Copyright", "type": TYPES.Ascii},
|
||||||
34377: {'name': 'ImageResources', 'type': TYPES.Byte},
|
33434: {"name": "ExposureTime", "type": TYPES.Rational},
|
||||||
34665: {'name': 'ExifTag', 'type': TYPES.Long},
|
34377: {"name": "ImageResources", "type": TYPES.Byte},
|
||||||
34675: {'name': 'InterColorProfile', 'type': TYPES.Undefined},
|
34665: {"name": "ExifTag", "type": TYPES.Long},
|
||||||
34853: {'name': 'GPSTag', 'type': TYPES.Long},
|
34675: {"name": "InterColorProfile", "type": TYPES.Undefined},
|
||||||
34857: {'name': 'Interlace', 'type': TYPES.Short},
|
34853: {"name": "GPSTag", "type": TYPES.Long},
|
||||||
34858: {'name': 'TimeZoneOffset', 'type': TYPES.Long},
|
34857: {"name": "Interlace", "type": TYPES.Short},
|
||||||
34859: {'name': 'SelfTimerMode', 'type': TYPES.Short},
|
34858: {"name": "TimeZoneOffset", "type": TYPES.Long},
|
||||||
37387: {'name': 'FlashEnergy', 'type': TYPES.Rational},
|
34859: {"name": "SelfTimerMode", "type": TYPES.Short},
|
||||||
37388: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined},
|
37387: {"name": "FlashEnergy", "type": TYPES.Rational},
|
||||||
37389: {'name': 'Noise', 'type': TYPES.Undefined},
|
37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
|
||||||
37390: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational},
|
37389: {"name": "Noise", "type": TYPES.Undefined},
|
||||||
37391: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational},
|
37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
|
||||||
37392: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short},
|
37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
|
||||||
37393: {'name': 'ImageNumber', 'type': TYPES.Long},
|
37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
|
||||||
37394: {'name': 'SecurityClassification', 'type': TYPES.Ascii},
|
37393: {"name": "ImageNumber", "type": TYPES.Long},
|
||||||
37395: {'name': 'ImageHistory', 'type': TYPES.Ascii},
|
37394: {"name": "SecurityClassification", "type": TYPES.Ascii},
|
||||||
37397: {'name': 'ExposureIndex', 'type': TYPES.Rational},
|
37395: {"name": "ImageHistory", "type": TYPES.Ascii},
|
||||||
37398: {'name': 'TIFFEPStandardID', 'type': TYPES.Byte},
|
37397: {"name": "ExposureIndex", "type": TYPES.Rational},
|
||||||
37399: {'name': 'SensingMethod', 'type': TYPES.Short},
|
37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte},
|
||||||
40091: {'name': 'XPTitle', 'type': TYPES.Byte},
|
37399: {"name": "SensingMethod", "type": TYPES.Short},
|
||||||
40092: {'name': 'XPComment', 'type': TYPES.Byte},
|
40091: {"name": "XPTitle", "type": TYPES.Byte},
|
||||||
40093: {'name': 'XPAuthor', 'type': TYPES.Byte},
|
40092: {"name": "XPComment", "type": TYPES.Byte},
|
||||||
40094: {'name': 'XPKeywords', 'type': TYPES.Byte},
|
40093: {"name": "XPAuthor", "type": TYPES.Byte},
|
||||||
40095: {'name': 'XPSubject', 'type': TYPES.Byte},
|
40094: {"name": "XPKeywords", "type": TYPES.Byte},
|
||||||
50341: {'name': 'PrintImageMatching', 'type': TYPES.Undefined},
|
40095: {"name": "XPSubject", "type": TYPES.Byte},
|
||||||
50706: {'name': 'DNGVersion', 'type': TYPES.Byte},
|
50341: {"name": "PrintImageMatching", "type": TYPES.Undefined},
|
||||||
50707: {'name': 'DNGBackwardVersion', 'type': TYPES.Byte},
|
50706: {"name": "DNGVersion", "type": TYPES.Byte},
|
||||||
50708: {'name': 'UniqueCameraModel', 'type': TYPES.Ascii},
|
50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte},
|
||||||
50709: {'name': 'LocalizedCameraModel', 'type': TYPES.Byte},
|
50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii},
|
||||||
50710: {'name': 'CFAPlaneColor', 'type': TYPES.Byte},
|
50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte},
|
||||||
50711: {'name': 'CFALayout', 'type': TYPES.Short},
|
50710: {"name": "CFAPlaneColor", "type": TYPES.Byte},
|
||||||
50712: {'name': 'LinearizationTable', 'type': TYPES.Short},
|
50711: {"name": "CFALayout", "type": TYPES.Short},
|
||||||
50713: {'name': 'BlackLevelRepeatDim', 'type': TYPES.Short},
|
50712: {"name": "LinearizationTable", "type": TYPES.Short},
|
||||||
50714: {'name': 'BlackLevel', 'type': TYPES.Rational},
|
50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short},
|
||||||
50715: {'name': 'BlackLevelDeltaH', 'type': TYPES.SRational},
|
50714: {"name": "BlackLevel", "type": TYPES.Rational},
|
||||||
50716: {'name': 'BlackLevelDeltaV', 'type': TYPES.SRational},
|
50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational},
|
||||||
50717: {'name': 'WhiteLevel', 'type': TYPES.Short},
|
50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational},
|
||||||
50718: {'name': 'DefaultScale', 'type': TYPES.Rational},
|
50717: {"name": "WhiteLevel", "type": TYPES.Short},
|
||||||
50719: {'name': 'DefaultCropOrigin', 'type': TYPES.Short},
|
50718: {"name": "DefaultScale", "type": TYPES.Rational},
|
||||||
50720: {'name': 'DefaultCropSize', 'type': TYPES.Short},
|
50719: {"name": "DefaultCropOrigin", "type": TYPES.Short},
|
||||||
50721: {'name': 'ColorMatrix1', 'type': TYPES.SRational},
|
50720: {"name": "DefaultCropSize", "type": TYPES.Short},
|
||||||
50722: {'name': 'ColorMatrix2', 'type': TYPES.SRational},
|
50721: {"name": "ColorMatrix1", "type": TYPES.SRational},
|
||||||
50723: {'name': 'CameraCalibration1', 'type': TYPES.SRational},
|
50722: {"name": "ColorMatrix2", "type": TYPES.SRational},
|
||||||
50724: {'name': 'CameraCalibration2', 'type': TYPES.SRational},
|
50723: {"name": "CameraCalibration1", "type": TYPES.SRational},
|
||||||
50725: {'name': 'ReductionMatrix1', 'type': TYPES.SRational},
|
50724: {"name": "CameraCalibration2", "type": TYPES.SRational},
|
||||||
50726: {'name': 'ReductionMatrix2', 'type': TYPES.SRational},
|
50725: {"name": "ReductionMatrix1", "type": TYPES.SRational},
|
||||||
50727: {'name': 'AnalogBalance', 'type': TYPES.Rational},
|
50726: {"name": "ReductionMatrix2", "type": TYPES.SRational},
|
||||||
50728: {'name': 'AsShotNeutral', 'type': TYPES.Short},
|
50727: {"name": "AnalogBalance", "type": TYPES.Rational},
|
||||||
50729: {'name': 'AsShotWhiteXY', 'type': TYPES.Rational},
|
50728: {"name": "AsShotNeutral", "type": TYPES.Short},
|
||||||
50730: {'name': 'BaselineExposure', 'type': TYPES.SRational},
|
50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational},
|
||||||
50731: {'name': 'BaselineNoise', 'type': TYPES.Rational},
|
50730: {"name": "BaselineExposure", "type": TYPES.SRational},
|
||||||
50732: {'name': 'BaselineSharpness', 'type': TYPES.Rational},
|
50731: {"name": "BaselineNoise", "type": TYPES.Rational},
|
||||||
50733: {'name': 'BayerGreenSplit', 'type': TYPES.Long},
|
50732: {"name": "BaselineSharpness", "type": TYPES.Rational},
|
||||||
50734: {'name': 'LinearResponseLimit', 'type': TYPES.Rational},
|
50733: {"name": "BayerGreenSplit", "type": TYPES.Long},
|
||||||
50735: {'name': 'CameraSerialNumber', 'type': TYPES.Ascii},
|
50734: {"name": "LinearResponseLimit", "type": TYPES.Rational},
|
||||||
50736: {'name': 'LensInfo', 'type': TYPES.Rational},
|
50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii},
|
||||||
50737: {'name': 'ChromaBlurRadius', 'type': TYPES.Rational},
|
50736: {"name": "LensInfo", "type": TYPES.Rational},
|
||||||
50738: {'name': 'AntiAliasStrength', 'type': TYPES.Rational},
|
50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational},
|
||||||
50739: {'name': 'ShadowScale', 'type': TYPES.SRational},
|
50738: {"name": "AntiAliasStrength", "type": TYPES.Rational},
|
||||||
50740: {'name': 'DNGPrivateData', 'type': TYPES.Byte},
|
50739: {"name": "ShadowScale", "type": TYPES.SRational},
|
||||||
50741: {'name': 'MakerNoteSafety', 'type': TYPES.Short},
|
50740: {"name": "DNGPrivateData", "type": TYPES.Byte},
|
||||||
50778: {'name': 'CalibrationIlluminant1', 'type': TYPES.Short},
|
50741: {"name": "MakerNoteSafety", "type": TYPES.Short},
|
||||||
50779: {'name': 'CalibrationIlluminant2', 'type': TYPES.Short},
|
50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short},
|
||||||
50780: {'name': 'BestQualityScale', 'type': TYPES.Rational},
|
50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short},
|
||||||
50781: {'name': 'RawDataUniqueID', 'type': TYPES.Byte},
|
50780: {"name": "BestQualityScale", "type": TYPES.Rational},
|
||||||
50827: {'name': 'OriginalRawFileName', 'type': TYPES.Byte},
|
50781: {"name": "RawDataUniqueID", "type": TYPES.Byte},
|
||||||
50828: {'name': 'OriginalRawFileData', 'type': TYPES.Undefined},
|
50827: {"name": "OriginalRawFileName", "type": TYPES.Byte},
|
||||||
50829: {'name': 'ActiveArea', 'type': TYPES.Short},
|
50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined},
|
||||||
50830: {'name': 'MaskedAreas', 'type': TYPES.Short},
|
50829: {"name": "ActiveArea", "type": TYPES.Short},
|
||||||
50831: {'name': 'AsShotICCProfile', 'type': TYPES.Undefined},
|
50830: {"name": "MaskedAreas", "type": TYPES.Short},
|
||||||
50832: {'name': 'AsShotPreProfileMatrix', 'type': TYPES.SRational},
|
50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined},
|
||||||
50833: {'name': 'CurrentICCProfile', 'type': TYPES.Undefined},
|
50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational},
|
||||||
50834: {'name': 'CurrentPreProfileMatrix', 'type': TYPES.SRational},
|
50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined},
|
||||||
50879: {'name': 'ColorimetricReference', 'type': TYPES.Short},
|
50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational},
|
||||||
50931: {'name': 'CameraCalibrationSignature', 'type': TYPES.Byte},
|
50879: {"name": "ColorimetricReference", "type": TYPES.Short},
|
||||||
50932: {'name': 'ProfileCalibrationSignature', 'type': TYPES.Byte},
|
50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte},
|
||||||
50934: {'name': 'AsShotProfileName', 'type': TYPES.Byte},
|
50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte},
|
||||||
50935: {'name': 'NoiseReductionApplied', 'type': TYPES.Rational},
|
50934: {"name": "AsShotProfileName", "type": TYPES.Byte},
|
||||||
50936: {'name': 'ProfileName', 'type': TYPES.Byte},
|
50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational},
|
||||||
50937: {'name': 'ProfileHueSatMapDims', 'type': TYPES.Long},
|
50936: {"name": "ProfileName", "type": TYPES.Byte},
|
||||||
50938: {'name': 'ProfileHueSatMapData1', 'type': TYPES.Float},
|
50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long},
|
||||||
50939: {'name': 'ProfileHueSatMapData2', 'type': TYPES.Float},
|
50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float},
|
||||||
50940: {'name': 'ProfileToneCurve', 'type': TYPES.Float},
|
50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float},
|
||||||
50941: {'name': 'ProfileEmbedPolicy', 'type': TYPES.Long},
|
50940: {"name": "ProfileToneCurve", "type": TYPES.Float},
|
||||||
50942: {'name': 'ProfileCopyright', 'type': TYPES.Byte},
|
50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long},
|
||||||
50964: {'name': 'ForwardMatrix1', 'type': TYPES.SRational},
|
50942: {"name": "ProfileCopyright", "type": TYPES.Byte},
|
||||||
50965: {'name': 'ForwardMatrix2', 'type': TYPES.SRational},
|
50964: {"name": "ForwardMatrix1", "type": TYPES.SRational},
|
||||||
50966: {'name': 'PreviewApplicationName', 'type': TYPES.Byte},
|
50965: {"name": "ForwardMatrix2", "type": TYPES.SRational},
|
||||||
50967: {'name': 'PreviewApplicationVersion', 'type': TYPES.Byte},
|
50966: {"name": "PreviewApplicationName", "type": TYPES.Byte},
|
||||||
50968: {'name': 'PreviewSettingsName', 'type': TYPES.Byte},
|
50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte},
|
||||||
50969: {'name': 'PreviewSettingsDigest', 'type': TYPES.Byte},
|
50968: {"name": "PreviewSettingsName", "type": TYPES.Byte},
|
||||||
50970: {'name': 'PreviewColorSpace', 'type': TYPES.Long},
|
50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte},
|
||||||
50971: {'name': 'PreviewDateTime', 'type': TYPES.Ascii},
|
50970: {"name": "PreviewColorSpace", "type": TYPES.Long},
|
||||||
50972: {'name': 'RawImageDigest', 'type': TYPES.Undefined},
|
50971: {"name": "PreviewDateTime", "type": TYPES.Ascii},
|
||||||
50973: {'name': 'OriginalRawFileDigest', 'type': TYPES.Undefined},
|
50972: {"name": "RawImageDigest", "type": TYPES.Undefined},
|
||||||
50974: {'name': 'SubTileBlockSize', 'type': TYPES.Long},
|
50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined},
|
||||||
50975: {'name': 'RowInterleaveFactor', 'type': TYPES.Long},
|
50974: {"name": "SubTileBlockSize", "type": TYPES.Long},
|
||||||
50981: {'name': 'ProfileLookTableDims', 'type': TYPES.Long},
|
50975: {"name": "RowInterleaveFactor", "type": TYPES.Long},
|
||||||
50982: {'name': 'ProfileLookTableData', 'type': TYPES.Float},
|
50981: {"name": "ProfileLookTableDims", "type": TYPES.Long},
|
||||||
51008: {'name': 'OpcodeList1', 'type': TYPES.Undefined},
|
50982: {"name": "ProfileLookTableData", "type": TYPES.Float},
|
||||||
51009: {'name': 'OpcodeList2', 'type': TYPES.Undefined},
|
51008: {"name": "OpcodeList1", "type": TYPES.Undefined},
|
||||||
51022: {'name': 'OpcodeList3', 'type': TYPES.Undefined},
|
51009: {"name": "OpcodeList2", "type": TYPES.Undefined},
|
||||||
60606: {'name': 'ZZZTestSlong1', 'type': TYPES.SLong},
|
51022: {"name": "OpcodeList3", "type": TYPES.Undefined},
|
||||||
60607: {'name': 'ZZZTestSlong2', 'type': TYPES.SLong},
|
60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong},
|
||||||
60608: {'name': 'ZZZTestSByte', 'type': TYPES.SByte},
|
60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong},
|
||||||
60609: {'name': 'ZZZTestSShort', 'type': TYPES.SShort},
|
60608: {"name": "ZZZTestSByte", "type": TYPES.SByte},
|
||||||
60610: {'name': 'ZZZTestDFloat', 'type': TYPES.DFloat},},
|
60609: {"name": "ZZZTestSShort", "type": TYPES.SShort},
|
||||||
'Exif': {33434: {'name': 'ExposureTime', 'type': TYPES.Rational},
|
60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat},
|
||||||
33437: {'name': 'FNumber', 'type': TYPES.Rational},
|
},
|
||||||
34850: {'name': 'ExposureProgram', 'type': TYPES.Short},
|
"Exif": {
|
||||||
34852: {'name': 'SpectralSensitivity', 'type': TYPES.Ascii},
|
33434: {"name": "ExposureTime", "type": TYPES.Rational},
|
||||||
34855: {'name': 'ISOSpeedRatings', 'type': TYPES.Short},
|
33437: {"name": "FNumber", "type": TYPES.Rational},
|
||||||
34856: {'name': 'OECF', 'type': TYPES.Undefined},
|
34850: {"name": "ExposureProgram", "type": TYPES.Short},
|
||||||
34864: {'name': 'SensitivityType', 'type': TYPES.Short},
|
34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii},
|
||||||
34865: {'name': 'StandardOutputSensitivity', 'type': TYPES.Long},
|
34855: {"name": "ISOSpeedRatings", "type": TYPES.Short},
|
||||||
34866: {'name': 'RecommendedExposureIndex', 'type': TYPES.Long},
|
34856: {"name": "OECF", "type": TYPES.Undefined},
|
||||||
34867: {'name': 'ISOSpeed', 'type': TYPES.Long},
|
34864: {"name": "SensitivityType", "type": TYPES.Short},
|
||||||
34868: {'name': 'ISOSpeedLatitudeyyy', 'type': TYPES.Long},
|
34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long},
|
||||||
34869: {'name': 'ISOSpeedLatitudezzz', 'type': TYPES.Long},
|
34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long},
|
||||||
36864: {'name': 'ExifVersion', 'type': TYPES.Undefined},
|
34867: {"name": "ISOSpeed", "type": TYPES.Long},
|
||||||
36867: {'name': 'DateTimeOriginal', 'type': TYPES.Ascii},
|
34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long},
|
||||||
36868: {'name': 'DateTimeDigitized', 'type': TYPES.Ascii},
|
34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long},
|
||||||
36880: {'name': 'OffsetTime', 'type': TYPES.Ascii},
|
36864: {"name": "ExifVersion", "type": TYPES.Undefined},
|
||||||
36881: {'name': 'OffsetTimeOriginal', 'type': TYPES.Ascii},
|
36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii},
|
||||||
36882: {'name': 'OffsetTimeDigitized', 'type': TYPES.Ascii},
|
36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii},
|
||||||
37121: {'name': 'ComponentsConfiguration', 'type': TYPES.Undefined},
|
36880: {"name": "OffsetTime", "type": TYPES.Ascii},
|
||||||
37122: {'name': 'CompressedBitsPerPixel', 'type': TYPES.Rational},
|
36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii},
|
||||||
37377: {'name': 'ShutterSpeedValue', 'type': TYPES.SRational},
|
36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii},
|
||||||
37378: {'name': 'ApertureValue', 'type': TYPES.Rational},
|
37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined},
|
||||||
37379: {'name': 'BrightnessValue', 'type': TYPES.SRational},
|
37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational},
|
||||||
37380: {'name': 'ExposureBiasValue', 'type': TYPES.SRational},
|
37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational},
|
||||||
37381: {'name': 'MaxApertureValue', 'type': TYPES.Rational},
|
37378: {"name": "ApertureValue", "type": TYPES.Rational},
|
||||||
37382: {'name': 'SubjectDistance', 'type': TYPES.Rational},
|
37379: {"name": "BrightnessValue", "type": TYPES.SRational},
|
||||||
37383: {'name': 'MeteringMode', 'type': TYPES.Short},
|
37380: {"name": "ExposureBiasValue", "type": TYPES.SRational},
|
||||||
37384: {'name': 'LightSource', 'type': TYPES.Short},
|
37381: {"name": "MaxApertureValue", "type": TYPES.Rational},
|
||||||
37385: {'name': 'Flash', 'type': TYPES.Short},
|
37382: {"name": "SubjectDistance", "type": TYPES.Rational},
|
||||||
37386: {'name': 'FocalLength', 'type': TYPES.Rational},
|
37383: {"name": "MeteringMode", "type": TYPES.Short},
|
||||||
37396: {'name': 'SubjectArea', 'type': TYPES.Short},
|
37384: {"name": "LightSource", "type": TYPES.Short},
|
||||||
37500: {'name': 'MakerNote', 'type': TYPES.Undefined},
|
37385: {"name": "Flash", "type": TYPES.Short},
|
||||||
37510: {'name': 'UserComment', 'type': TYPES.Undefined},
|
37386: {"name": "FocalLength", "type": TYPES.Rational},
|
||||||
37520: {'name': 'SubSecTime', 'type': TYPES.Ascii},
|
37396: {"name": "SubjectArea", "type": TYPES.Short},
|
||||||
37521: {'name': 'SubSecTimeOriginal', 'type': TYPES.Ascii},
|
37500: {"name": "MakerNote", "type": TYPES.Undefined},
|
||||||
37522: {'name': 'SubSecTimeDigitized', 'type': TYPES.Ascii},
|
37510: {"name": "UserComment", "type": TYPES.Undefined},
|
||||||
37888: {'name': 'Temperature', 'type': TYPES.SRational},
|
37520: {"name": "SubSecTime", "type": TYPES.Ascii},
|
||||||
37889: {'name': 'Humidity', 'type': TYPES.Rational},
|
37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii},
|
||||||
37890: {'name': 'Pressure', 'type': TYPES.Rational},
|
37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii},
|
||||||
37891: {'name': 'WaterDepth', 'type': TYPES.SRational},
|
37888: {"name": "Temperature", "type": TYPES.SRational},
|
||||||
37892: {'name': 'Acceleration', 'type': TYPES.Rational},
|
37889: {"name": "Humidity", "type": TYPES.Rational},
|
||||||
37893: {'name': 'CameraElevationAngle', 'type': TYPES.SRational},
|
37890: {"name": "Pressure", "type": TYPES.Rational},
|
||||||
40960: {'name': 'FlashpixVersion', 'type': TYPES.Undefined},
|
37891: {"name": "WaterDepth", "type": TYPES.SRational},
|
||||||
40961: {'name': 'ColorSpace', 'type': TYPES.Short},
|
37892: {"name": "Acceleration", "type": TYPES.Rational},
|
||||||
40962: {'name': 'PixelXDimension', 'type': TYPES.Long},
|
37893: {"name": "CameraElevationAngle", "type": TYPES.SRational},
|
||||||
40963: {'name': 'PixelYDimension', 'type': TYPES.Long},
|
40960: {"name": "FlashpixVersion", "type": TYPES.Undefined},
|
||||||
40964: {'name': 'RelatedSoundFile', 'type': TYPES.Ascii},
|
40961: {"name": "ColorSpace", "type": TYPES.Short},
|
||||||
40965: {'name': 'InteroperabilityTag', 'type': TYPES.Long},
|
40962: {"name": "PixelXDimension", "type": TYPES.Long},
|
||||||
41483: {'name': 'FlashEnergy', 'type': TYPES.Rational},
|
40963: {"name": "PixelYDimension", "type": TYPES.Long},
|
||||||
41484: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined},
|
40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii},
|
||||||
41486: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational},
|
40965: {"name": "InteroperabilityTag", "type": TYPES.Long},
|
||||||
41487: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational},
|
41483: {"name": "FlashEnergy", "type": TYPES.Rational},
|
||||||
41488: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short},
|
41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
|
||||||
41492: {'name': 'SubjectLocation', 'type': TYPES.Short},
|
41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
|
||||||
41493: {'name': 'ExposureIndex', 'type': TYPES.Rational},
|
41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
|
||||||
41495: {'name': 'SensingMethod', 'type': TYPES.Short},
|
41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
|
||||||
41728: {'name': 'FileSource', 'type': TYPES.Undefined},
|
41492: {"name": "SubjectLocation", "type": TYPES.Short},
|
||||||
41729: {'name': 'SceneType', 'type': TYPES.Undefined},
|
41493: {"name": "ExposureIndex", "type": TYPES.Rational},
|
||||||
41730: {'name': 'CFAPattern', 'type': TYPES.Undefined},
|
41495: {"name": "SensingMethod", "type": TYPES.Short},
|
||||||
41985: {'name': 'CustomRendered', 'type': TYPES.Short},
|
41728: {"name": "FileSource", "type": TYPES.Undefined},
|
||||||
41986: {'name': 'ExposureMode', 'type': TYPES.Short},
|
41729: {"name": "SceneType", "type": TYPES.Undefined},
|
||||||
41987: {'name': 'WhiteBalance', 'type': TYPES.Short},
|
41730: {"name": "CFAPattern", "type": TYPES.Undefined},
|
||||||
41988: {'name': 'DigitalZoomRatio', 'type': TYPES.Rational},
|
41985: {"name": "CustomRendered", "type": TYPES.Short},
|
||||||
41989: {'name': 'FocalLengthIn35mmFilm', 'type': TYPES.Short},
|
41986: {"name": "ExposureMode", "type": TYPES.Short},
|
||||||
41990: {'name': 'SceneCaptureType', 'type': TYPES.Short},
|
41987: {"name": "WhiteBalance", "type": TYPES.Short},
|
||||||
41991: {'name': 'GainControl', 'type': TYPES.Short},
|
41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational},
|
||||||
41992: {'name': 'Contrast', 'type': TYPES.Short},
|
41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short},
|
||||||
41993: {'name': 'Saturation', 'type': TYPES.Short},
|
41990: {"name": "SceneCaptureType", "type": TYPES.Short},
|
||||||
41994: {'name': 'Sharpness', 'type': TYPES.Short},
|
41991: {"name": "GainControl", "type": TYPES.Short},
|
||||||
41995: {'name': 'DeviceSettingDescription', 'type': TYPES.Undefined},
|
41992: {"name": "Contrast", "type": TYPES.Short},
|
||||||
41996: {'name': 'SubjectDistanceRange', 'type': TYPES.Short},
|
41993: {"name": "Saturation", "type": TYPES.Short},
|
||||||
42016: {'name': 'ImageUniqueID', 'type': TYPES.Ascii},
|
41994: {"name": "Sharpness", "type": TYPES.Short},
|
||||||
42032: {'name': 'CameraOwnerName', 'type': TYPES.Ascii},
|
41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined},
|
||||||
42033: {'name': 'BodySerialNumber', 'type': TYPES.Ascii},
|
41996: {"name": "SubjectDistanceRange", "type": TYPES.Short},
|
||||||
42034: {'name': 'LensSpecification', 'type': TYPES.Rational},
|
42016: {"name": "ImageUniqueID", "type": TYPES.Ascii},
|
||||||
42035: {'name': 'LensMake', 'type': TYPES.Ascii},
|
42032: {"name": "CameraOwnerName", "type": TYPES.Ascii},
|
||||||
42036: {'name': 'LensModel', 'type': TYPES.Ascii},
|
42033: {"name": "BodySerialNumber", "type": TYPES.Ascii},
|
||||||
42037: {'name': 'LensSerialNumber', 'type': TYPES.Ascii},
|
42034: {"name": "LensSpecification", "type": TYPES.Rational},
|
||||||
42240: {'name': 'Gamma', 'type': TYPES.Rational}},
|
42035: {"name": "LensMake", "type": TYPES.Ascii},
|
||||||
'GPS': {0: {'name': 'GPSVersionID', 'type': TYPES.Byte},
|
42036: {"name": "LensModel", "type": TYPES.Ascii},
|
||||||
1: {'name': 'GPSLatitudeRef', 'type': TYPES.Ascii},
|
42037: {"name": "LensSerialNumber", "type": TYPES.Ascii},
|
||||||
2: {'name': 'GPSLatitude', 'type': TYPES.Rational},
|
42240: {"name": "Gamma", "type": TYPES.Rational},
|
||||||
3: {'name': 'GPSLongitudeRef', 'type': TYPES.Ascii},
|
},
|
||||||
4: {'name': 'GPSLongitude', 'type': TYPES.Rational},
|
"GPS": {
|
||||||
5: {'name': 'GPSAltitudeRef', 'type': TYPES.Byte},
|
0: {"name": "GPSVersionID", "type": TYPES.Byte},
|
||||||
6: {'name': 'GPSAltitude', 'type': TYPES.Rational},
|
1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii},
|
||||||
7: {'name': 'GPSTimeStamp', 'type': TYPES.Rational},
|
2: {"name": "GPSLatitude", "type": TYPES.Rational},
|
||||||
8: {'name': 'GPSSatellites', 'type': TYPES.Ascii},
|
3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii},
|
||||||
9: {'name': 'GPSStatus', 'type': TYPES.Ascii},
|
4: {"name": "GPSLongitude", "type": TYPES.Rational},
|
||||||
10: {'name': 'GPSMeasureMode', 'type': TYPES.Ascii},
|
5: {"name": "GPSAltitudeRef", "type": TYPES.Byte},
|
||||||
11: {'name': 'GPSDOP', 'type': TYPES.Rational},
|
6: {"name": "GPSAltitude", "type": TYPES.Rational},
|
||||||
12: {'name': 'GPSSpeedRef', 'type': TYPES.Ascii},
|
7: {"name": "GPSTimeStamp", "type": TYPES.Rational},
|
||||||
13: {'name': 'GPSSpeed', 'type': TYPES.Rational},
|
8: {"name": "GPSSatellites", "type": TYPES.Ascii},
|
||||||
14: {'name': 'GPSTrackRef', 'type': TYPES.Ascii},
|
9: {"name": "GPSStatus", "type": TYPES.Ascii},
|
||||||
15: {'name': 'GPSTrack', 'type': TYPES.Rational},
|
10: {"name": "GPSMeasureMode", "type": TYPES.Ascii},
|
||||||
16: {'name': 'GPSImgDirectionRef', 'type': TYPES.Ascii},
|
11: {"name": "GPSDOP", "type": TYPES.Rational},
|
||||||
17: {'name': 'GPSImgDirection', 'type': TYPES.Rational},
|
12: {"name": "GPSSpeedRef", "type": TYPES.Ascii},
|
||||||
18: {'name': 'GPSMapDatum', 'type': TYPES.Ascii},
|
13: {"name": "GPSSpeed", "type": TYPES.Rational},
|
||||||
19: {'name': 'GPSDestLatitudeRef', 'type': TYPES.Ascii},
|
14: {"name": "GPSTrackRef", "type": TYPES.Ascii},
|
||||||
20: {'name': 'GPSDestLatitude', 'type': TYPES.Rational},
|
15: {"name": "GPSTrack", "type": TYPES.Rational},
|
||||||
21: {'name': 'GPSDestLongitudeRef', 'type': TYPES.Ascii},
|
16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii},
|
||||||
22: {'name': 'GPSDestLongitude', 'type': TYPES.Rational},
|
17: {"name": "GPSImgDirection", "type": TYPES.Rational},
|
||||||
23: {'name': 'GPSDestBearingRef', 'type': TYPES.Ascii},
|
18: {"name": "GPSMapDatum", "type": TYPES.Ascii},
|
||||||
24: {'name': 'GPSDestBearing', 'type': TYPES.Rational},
|
19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii},
|
||||||
25: {'name': 'GPSDestDistanceRef', 'type': TYPES.Ascii},
|
20: {"name": "GPSDestLatitude", "type": TYPES.Rational},
|
||||||
26: {'name': 'GPSDestDistance', 'type': TYPES.Rational},
|
21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii},
|
||||||
27: {'name': 'GPSProcessingMethod', 'type': TYPES.Undefined},
|
22: {"name": "GPSDestLongitude", "type": TYPES.Rational},
|
||||||
28: {'name': 'GPSAreaInformation', 'type': TYPES.Undefined},
|
23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii},
|
||||||
29: {'name': 'GPSDateStamp', 'type': TYPES.Ascii},
|
24: {"name": "GPSDestBearing", "type": TYPES.Rational},
|
||||||
30: {'name': 'GPSDifferential', 'type': TYPES.Short},
|
25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii},
|
||||||
31: {'name': 'GPSHPositioningError', 'type': TYPES.Rational}},
|
26: {"name": "GPSDestDistance", "type": TYPES.Rational},
|
||||||
'Interop': {1: {'name': 'InteroperabilityIndex', 'type': TYPES.Ascii}},
|
27: {"name": "GPSProcessingMethod", "type": TYPES.Undefined},
|
||||||
|
28: {"name": "GPSAreaInformation", "type": TYPES.Undefined},
|
||||||
|
29: {"name": "GPSDateStamp", "type": TYPES.Ascii},
|
||||||
|
30: {"name": "GPSDifferential", "type": TYPES.Short},
|
||||||
|
31: {"name": "GPSHPositioningError", "type": TYPES.Rational},
|
||||||
|
},
|
||||||
|
"Interop": {1: {"name": "InteroperabilityIndex", "type": TYPES.Ascii}},
|
||||||
}
|
}
|
||||||
|
|
||||||
TAGS["0th"] = TAGS["Image"]
|
TAGS["0th"] = TAGS["Image"]
|
||||||
TAGS["1st"] = TAGS["Image"]
|
TAGS["1st"] = TAGS["Image"]
|
||||||
|
|
||||||
|
|
||||||
class ImageIFD:
|
class ImageIFD:
|
||||||
"""Exif tag number reference - 0th IFD"""
|
"""Exif tag number reference - 0th IFD"""
|
||||||
|
|
||||||
ProcessingSoftware = 11
|
ProcessingSoftware = 11
|
||||||
NewSubfileType = 254
|
NewSubfileType = 254
|
||||||
SubfileType = 255
|
SubfileType = 255
|
||||||
|
|
@ -516,6 +524,7 @@ class ImageIFD:
|
||||||
|
|
||||||
class ExifIFD:
|
class ExifIFD:
|
||||||
"""Exif tag number reference - Exif IFD"""
|
"""Exif tag number reference - Exif IFD"""
|
||||||
|
|
||||||
ExposureTime = 33434
|
ExposureTime = 33434
|
||||||
FNumber = 33437
|
FNumber = 33437
|
||||||
ExposureProgram = 34850
|
ExposureProgram = 34850
|
||||||
|
|
@ -599,6 +608,7 @@ class ExifIFD:
|
||||||
|
|
||||||
class GPSIFD:
|
class GPSIFD:
|
||||||
"""Exif tag number reference - GPS IFD"""
|
"""Exif tag number reference - GPS IFD"""
|
||||||
|
|
||||||
GPSVersionID = 0
|
GPSVersionID = 0
|
||||||
GPSLatitudeRef = 1
|
GPSLatitudeRef = 1
|
||||||
GPSLatitude = 2
|
GPSLatitude = 2
|
||||||
|
|
@ -635,4 +645,5 @@ class GPSIFD:
|
||||||
|
|
||||||
class InteropIFD:
|
class InteropIFD:
|
||||||
"""Exif tag number reference - Interoperability IFD"""
|
"""Exif tag number reference - Interoperability IFD"""
|
||||||
|
|
||||||
InteroperabilityIndex = 1
|
InteroperabilityIndex = 1
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from ._common import *
|
||||||
from ._exceptions import InvalidImageDataError
|
from ._exceptions import InvalidImageDataError
|
||||||
from . import _webp
|
from . import _webp
|
||||||
|
|
||||||
|
|
||||||
def insert(exif, image, new_file=None):
|
def insert(exif, image, new_file=None):
|
||||||
"""
|
"""
|
||||||
py:function:: piexif.insert(exif_bytes, filename)
|
py:function:: piexif.insert(exif_bytes, filename)
|
||||||
|
|
@ -20,7 +21,7 @@ def insert(exif, image, new_file=None):
|
||||||
|
|
||||||
output_file = False
|
output_file = False
|
||||||
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
||||||
maybe_image = sys.version_info >= (3,0,0) or isinstance(image, str)
|
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(image, str)
|
||||||
|
|
||||||
if maybe_image and image[0:2] == b"\xff\xd8":
|
if maybe_image and image[0:2] == b"\xff\xd8":
|
||||||
image_data = image
|
image_data = image
|
||||||
|
|
@ -29,7 +30,7 @@ def insert(exif, image, new_file=None):
|
||||||
image_data = image
|
image_data = image
|
||||||
file_type = "webp"
|
file_type = "webp"
|
||||||
else:
|
else:
|
||||||
with open(image, 'rb') as f:
|
with open(image, "rb") as f:
|
||||||
image_data = f.read()
|
image_data = f.read()
|
||||||
if image_data[0:2] == b"\xff\xd8":
|
if image_data[0:2] == b"\xff\xd8":
|
||||||
file_type = "jpeg"
|
file_type = "jpeg"
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,14 @@ def load(input_data, key_is_name=False):
|
||||||
:return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
|
:return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
exif_dict = {"0th":{},
|
exif_dict = {
|
||||||
"Exif":{},
|
"0th": {},
|
||||||
"GPS":{},
|
"Exif": {},
|
||||||
"Interop":{},
|
"GPS": {},
|
||||||
"1st":{},
|
"Interop": {},
|
||||||
"thumbnail":None}
|
"1st": {},
|
||||||
|
"thumbnail": None,
|
||||||
|
}
|
||||||
exifReader = _ExifReader(input_data)
|
exifReader = _ExifReader(input_data)
|
||||||
if exifReader.tiftag is None:
|
if exifReader.tiftag is None:
|
||||||
return exif_dict
|
return exif_dict
|
||||||
|
|
@ -34,8 +36,7 @@ def load(input_data, key_is_name=False):
|
||||||
else:
|
else:
|
||||||
exifReader.endian_mark = ">"
|
exifReader.endian_mark = ">"
|
||||||
|
|
||||||
pointer = struct.unpack(exifReader.endian_mark + "L",
|
pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0]
|
||||||
exifReader.tiftag[4:8])[0]
|
|
||||||
exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th")
|
exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th")
|
||||||
first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer")
|
first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer")
|
||||||
if ImageIFD.ExifTag in exif_dict["0th"]:
|
if ImageIFD.ExifTag in exif_dict["0th"]:
|
||||||
|
|
@ -48,14 +49,19 @@ def load(input_data, key_is_name=False):
|
||||||
pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag]
|
pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag]
|
||||||
exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop")
|
exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop")
|
||||||
if first_ifd_pointer != b"\x00\x00\x00\x00":
|
if first_ifd_pointer != b"\x00\x00\x00\x00":
|
||||||
pointer = struct.unpack(exifReader.endian_mark + "L",
|
pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0]
|
||||||
first_ifd_pointer)[0]
|
|
||||||
exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st")
|
exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st")
|
||||||
if (ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] and
|
if (
|
||||||
ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]):
|
ImageIFD.JPEGInterchangeFormat in exif_dict["1st"]
|
||||||
end = (exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] +
|
and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]
|
||||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength])
|
):
|
||||||
thumb = exifReader.tiftag[exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]:end]
|
end = (
|
||||||
|
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]
|
||||||
|
+ exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]
|
||||||
|
)
|
||||||
|
thumb = exifReader.tiftag[
|
||||||
|
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end
|
||||||
|
]
|
||||||
exif_dict["thumbnail"] = thumb
|
exif_dict["thumbnail"] = thumb
|
||||||
|
|
||||||
if key_is_name:
|
if key_is_name:
|
||||||
|
|
@ -66,7 +72,7 @@ def load(input_data, key_is_name=False):
|
||||||
class _ExifReader(object):
|
class _ExifReader(object):
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
||||||
maybe_image = sys.version_info >= (3,0,0) or isinstance(data, str)
|
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(data, str)
|
||||||
|
|
||||||
if maybe_image and data[0:2] == b"\xff\xd8": # JPEG
|
if maybe_image and data[0:2] == b"\xff\xd8": # JPEG
|
||||||
segments = split_into_segments(data)
|
segments = split_into_segments(data)
|
||||||
|
|
@ -82,7 +88,7 @@ class _ExifReader(object):
|
||||||
elif maybe_image and data[0:4] == b"Exif": # Exif
|
elif maybe_image and data[0:4] == b"Exif": # Exif
|
||||||
self.tiftag = data[6:]
|
self.tiftag = data[6:]
|
||||||
else:
|
else:
|
||||||
with open(data, 'rb') as f:
|
with open(data, "rb") as f:
|
||||||
magic_number = f.read(2)
|
magic_number = f.read(2)
|
||||||
if magic_number == b"\xff\xd8": # JPEG
|
if magic_number == b"\xff\xd8": # JPEG
|
||||||
app1 = read_exif_from_file(data)
|
app1 = read_exif_from_file(data)
|
||||||
|
|
@ -91,13 +97,13 @@ class _ExifReader(object):
|
||||||
else:
|
else:
|
||||||
self.tiftag = None
|
self.tiftag = None
|
||||||
elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF
|
elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF
|
||||||
with open(data, 'rb') as f:
|
with open(data, "rb") as f:
|
||||||
self.tiftag = f.read()
|
self.tiftag = f.read()
|
||||||
else:
|
else:
|
||||||
with open(data, 'rb') as f:
|
with open(data, "rb") as f:
|
||||||
header = f.read(12)
|
header = f.read(12)
|
||||||
if header[0:4] == b"RIFF"and header[8:12] == b"WEBP":
|
if header[0:4] == b"RIFF" and header[8:12] == b"WEBP":
|
||||||
with open(data, 'rb') as f:
|
with open(data, "rb") as f:
|
||||||
file_data = f.read()
|
file_data = f.read()
|
||||||
self.tiftag = _webp.get_exif(file_data)
|
self.tiftag = _webp.get_exif(file_data)
|
||||||
else:
|
else:
|
||||||
|
|
@ -105,8 +111,9 @@ class _ExifReader(object):
|
||||||
|
|
||||||
def get_ifd_dict(self, pointer, ifd_name, read_unknown=False):
|
def get_ifd_dict(self, pointer, ifd_name, read_unknown=False):
|
||||||
ifd_dict = {}
|
ifd_dict = {}
|
||||||
tag_count = struct.unpack(self.endian_mark + "H",
|
tag_count = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+2])[0]
|
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
|
||||||
|
)[0]
|
||||||
offset = pointer + 2
|
offset = pointer + 2
|
||||||
if ifd_name in ["0th", "1st"]:
|
if ifd_name in ["0th", "1st"]:
|
||||||
t = "Image"
|
t = "Image"
|
||||||
|
|
@ -115,26 +122,28 @@ class _ExifReader(object):
|
||||||
p_and_value = []
|
p_and_value = []
|
||||||
for x in range(tag_count):
|
for x in range(tag_count):
|
||||||
pointer = offset + 12 * x
|
pointer = offset + 12 * x
|
||||||
tag = struct.unpack(self.endian_mark + "H",
|
tag = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+2])[0]
|
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
|
||||||
value_type = struct.unpack(self.endian_mark + "H",
|
)[0]
|
||||||
self.tiftag[pointer + 2: pointer + 4])[0]
|
value_type = struct.unpack(
|
||||||
value_num = struct.unpack(self.endian_mark + "L",
|
self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4]
|
||||||
self.tiftag[pointer + 4: pointer + 8]
|
)[0]
|
||||||
)[0]
|
value_num = struct.unpack(
|
||||||
value = self.tiftag[pointer+8: pointer+12]
|
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
|
||||||
|
)[0]
|
||||||
|
value = self.tiftag[pointer + 8 : pointer + 12]
|
||||||
p_and_value.append((pointer, value_type, value_num, value))
|
p_and_value.append((pointer, value_type, value_num, value))
|
||||||
v_set = (value_type, value_num, value, tag)
|
v_set = (value_type, value_num, value, tag)
|
||||||
if tag in TAGS[t]:
|
if tag in TAGS[t]:
|
||||||
ifd_dict[tag] = self.convert_value(v_set)
|
ifd_dict[tag] = self.convert_value(v_set)
|
||||||
elif read_unknown:
|
elif read_unknown:
|
||||||
ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag)
|
ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag)
|
||||||
#else:
|
# else:
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
if ifd_name == "0th":
|
if ifd_name == "0th":
|
||||||
pointer = offset + 12 * tag_count
|
pointer = offset + 12 * tag_count
|
||||||
ifd_dict["first_ifd_pointer"] = self.tiftag[pointer:pointer + 4]
|
ifd_dict["first_ifd_pointer"] = self.tiftag[pointer : pointer + 4]
|
||||||
return ifd_dict
|
return ifd_dict
|
||||||
|
|
||||||
def convert_value(self, val):
|
def convert_value(self, val):
|
||||||
|
|
@ -143,114 +152,148 @@ class _ExifReader(object):
|
||||||
length = val[1]
|
length = val[1]
|
||||||
value = val[2]
|
value = val[2]
|
||||||
|
|
||||||
if t == TYPES.Byte: # BYTE
|
if t == TYPES.Byte: # BYTE
|
||||||
if length > 4:
|
if length > 4:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack("B" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer + length])
|
"B" * length, self.tiftag[pointer : pointer + length]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack("B" * length, value[0:length])
|
data = struct.unpack("B" * length, value[0:length])
|
||||||
elif t == TYPES.Ascii: # ASCII
|
elif t == TYPES.Ascii: # ASCII
|
||||||
if length > 4:
|
if length > 4:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = self.tiftag[pointer: pointer+length - 1]
|
data = self.tiftag[pointer : pointer + length - 1]
|
||||||
else:
|
else:
|
||||||
data = value[0: length - 1]
|
data = value[0 : length - 1]
|
||||||
elif t == TYPES.Short: # SHORT
|
elif t == TYPES.Short: # SHORT
|
||||||
if length > 2:
|
if length > 2:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "H" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*2])
|
self.endian_mark + "H" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 2],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack(self.endian_mark + "H" * length,
|
data = struct.unpack(
|
||||||
value[0:length * 2])
|
self.endian_mark + "H" * length, value[0 : length * 2]
|
||||||
elif t == TYPES.Long: # LONG
|
)
|
||||||
|
elif t == TYPES.Long: # LONG
|
||||||
if length > 1:
|
if length > 1:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "L" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*4])
|
self.endian_mark + "L" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 4],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack(self.endian_mark + "L" * length,
|
data = struct.unpack(self.endian_mark + "L" * length, value)
|
||||||
value)
|
elif t == TYPES.Rational: # RATIONAL
|
||||||
elif t == TYPES.Rational: # RATIONAL
|
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
if length > 1:
|
if length > 1:
|
||||||
data = tuple(
|
data = tuple(
|
||||||
(struct.unpack(self.endian_mark + "L",
|
(
|
||||||
self.tiftag[pointer + x * 8:
|
struct.unpack(
|
||||||
pointer + 4 + x * 8])[0],
|
self.endian_mark + "L",
|
||||||
struct.unpack(self.endian_mark + "L",
|
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
|
||||||
self.tiftag[pointer + 4 + x * 8:
|
)[0],
|
||||||
pointer + 8 + x * 8])[0])
|
struct.unpack(
|
||||||
|
self.endian_mark + "L",
|
||||||
|
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
|
||||||
|
)[0],
|
||||||
|
)
|
||||||
for x in range(length)
|
for x in range(length)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = (struct.unpack(self.endian_mark + "L",
|
data = (
|
||||||
self.tiftag[pointer: pointer + 4])[0],
|
struct.unpack(
|
||||||
struct.unpack(self.endian_mark + "L",
|
self.endian_mark + "L", self.tiftag[pointer : pointer + 4]
|
||||||
self.tiftag[pointer + 4: pointer + 8]
|
)[0],
|
||||||
)[0])
|
struct.unpack(
|
||||||
elif t == TYPES.SByte: # SIGNED BYTES
|
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
|
||||||
|
)[0],
|
||||||
|
)
|
||||||
|
elif t == TYPES.SByte: # SIGNED BYTES
|
||||||
if length > 4:
|
if length > 4:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack("b" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer + length])
|
"b" * length, self.tiftag[pointer : pointer + length]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack("b" * length, value[0:length])
|
data = struct.unpack("b" * length, value[0:length])
|
||||||
elif t == TYPES.Undefined: # UNDEFINED BYTES
|
elif t == TYPES.Undefined: # UNDEFINED BYTES
|
||||||
if length > 4:
|
if length > 4:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = self.tiftag[pointer: pointer+length]
|
data = self.tiftag[pointer : pointer + length]
|
||||||
else:
|
else:
|
||||||
data = value[0:length]
|
data = value[0:length]
|
||||||
elif t == TYPES.SShort: # SIGNED SHORT
|
elif t == TYPES.SShort: # SIGNED SHORT
|
||||||
if length > 2:
|
if length > 2:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "h" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*2])
|
self.endian_mark + "h" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 2],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack(self.endian_mark + "h" * length,
|
data = struct.unpack(
|
||||||
value[0:length * 2])
|
self.endian_mark + "h" * length, value[0 : length * 2]
|
||||||
elif t == TYPES.SLong: # SLONG
|
)
|
||||||
|
elif t == TYPES.SLong: # SLONG
|
||||||
if length > 1:
|
if length > 1:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "l" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*4])
|
self.endian_mark + "l" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 4],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack(self.endian_mark + "l" * length,
|
data = struct.unpack(self.endian_mark + "l" * length, value)
|
||||||
value)
|
elif t == TYPES.SRational: # SRATIONAL
|
||||||
elif t == TYPES.SRational: # SRATIONAL
|
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
if length > 1:
|
if length > 1:
|
||||||
data = tuple(
|
data = tuple(
|
||||||
(struct.unpack(self.endian_mark + "l",
|
(
|
||||||
self.tiftag[pointer + x * 8: pointer + 4 + x * 8])[0],
|
struct.unpack(
|
||||||
struct.unpack(self.endian_mark + "l",
|
self.endian_mark + "l",
|
||||||
self.tiftag[pointer + 4 + x * 8: pointer + 8 + x * 8])[0])
|
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
|
||||||
for x in range(length)
|
)[0],
|
||||||
|
struct.unpack(
|
||||||
|
self.endian_mark + "l",
|
||||||
|
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
|
||||||
|
)[0],
|
||||||
|
)
|
||||||
|
for x in range(length)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = (struct.unpack(self.endian_mark + "l",
|
data = (
|
||||||
self.tiftag[pointer: pointer + 4])[0],
|
struct.unpack(
|
||||||
struct.unpack(self.endian_mark + "l",
|
self.endian_mark + "l", self.tiftag[pointer : pointer + 4]
|
||||||
self.tiftag[pointer + 4: pointer + 8]
|
)[0],
|
||||||
)[0])
|
struct.unpack(
|
||||||
elif t == TYPES.Float: # FLOAT
|
self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8]
|
||||||
|
)[0],
|
||||||
|
)
|
||||||
|
elif t == TYPES.Float: # FLOAT
|
||||||
if length > 1:
|
if length > 1:
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "f" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*4])
|
self.endian_mark + "f" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 4],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = struct.unpack(self.endian_mark + "f" * length,
|
data = struct.unpack(self.endian_mark + "f" * length, value)
|
||||||
value)
|
elif t == TYPES.DFloat: # DOUBLE
|
||||||
elif t == TYPES.DFloat: # DOUBLE
|
|
||||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||||
data = struct.unpack(self.endian_mark + "d" * length,
|
data = struct.unpack(
|
||||||
self.tiftag[pointer: pointer+length*8])
|
self.endian_mark + "d" * length,
|
||||||
|
self.tiftag[pointer : pointer + length * 8],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Exif might be wrong. Got incorrect value " +
|
raise ValueError(
|
||||||
"type to decode.\n" +
|
"Exif might be wrong. Got incorrect value "
|
||||||
"tag: " + str(val[3]) + "\ntype: " + str(t))
|
+ "type to decode.\n"
|
||||||
|
+ "tag: "
|
||||||
|
+ str(val[3])
|
||||||
|
+ "\ntype: "
|
||||||
|
+ str(t)
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(data, tuple) and (len(data) == 1):
|
if isinstance(data, tuple) and (len(data) == 1):
|
||||||
return data[0]
|
return data[0]
|
||||||
|
|
@ -260,11 +303,20 @@ class _ExifReader(object):
|
||||||
|
|
||||||
def _get_key_name_dict(exif_dict):
|
def _get_key_name_dict(exif_dict):
|
||||||
new_dict = {
|
new_dict = {
|
||||||
"0th":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["0th"].items()},
|
"0th": {
|
||||||
"Exif":{TAGS["Exif"][n]["name"]:value for n, value in exif_dict["Exif"].items()},
|
TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].items()
|
||||||
"1st":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["1st"].items()},
|
},
|
||||||
"GPS":{TAGS["GPS"][n]["name"]:value for n, value in exif_dict["GPS"].items()},
|
"Exif": {
|
||||||
"Interop":{TAGS["Interop"][n]["name"]:value for n, value in exif_dict["Interop"].items()},
|
TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items()
|
||||||
"thumbnail":exif_dict["thumbnail"],
|
},
|
||||||
|
"1st": {
|
||||||
|
TAGS["Image"][n]["name"]: value for n, value in exif_dict["1st"].items()
|
||||||
|
},
|
||||||
|
"GPS": {TAGS["GPS"][n]["name"]: value for n, value in exif_dict["GPS"].items()},
|
||||||
|
"Interop": {
|
||||||
|
TAGS["Interop"][n]["name"]: value
|
||||||
|
for n, value in exif_dict["Interop"].items()
|
||||||
|
},
|
||||||
|
"thumbnail": exif_dict["thumbnail"],
|
||||||
}
|
}
|
||||||
return new_dict
|
return new_dict
|
||||||
|
|
@ -3,6 +3,7 @@ import io
|
||||||
from ._common import *
|
from ._common import *
|
||||||
from . import _webp
|
from . import _webp
|
||||||
|
|
||||||
|
|
||||||
def remove(src, new_file=None):
|
def remove(src, new_file=None):
|
||||||
"""
|
"""
|
||||||
py:function:: piexif.remove(filename)
|
py:function:: piexif.remove(filename)
|
||||||
|
|
@ -19,7 +20,7 @@ def remove(src, new_file=None):
|
||||||
src_data = src
|
src_data = src
|
||||||
file_type = "webp"
|
file_type = "webp"
|
||||||
else:
|
else:
|
||||||
with open(src, 'rb') as f:
|
with open(src, "rb") as f:
|
||||||
src_data = f.read()
|
src_data = f.read()
|
||||||
output_is_file = True
|
output_is_file = True
|
||||||
if src_data[0:2] == b"\xff\xd8":
|
if src_data[0:2] == b"\xff\xd8":
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ def transplant(exif_src, image, new_file=None):
|
||||||
if exif_src[0:2] == b"\xff\xd8":
|
if exif_src[0:2] == b"\xff\xd8":
|
||||||
src_data = exif_src
|
src_data = exif_src
|
||||||
else:
|
else:
|
||||||
with open(exif_src, 'rb') as f:
|
with open(exif_src, "rb") as f:
|
||||||
src_data = f.read()
|
src_data = f.read()
|
||||||
segments = split_into_segments(src_data)
|
segments = split_into_segments(src_data)
|
||||||
exif = get_exif_seg(segments)
|
exif = get_exif_seg(segments)
|
||||||
|
|
@ -26,7 +26,7 @@ def transplant(exif_src, image, new_file=None):
|
||||||
if image[0:2] == b"\xff\xd8":
|
if image[0:2] == b"\xff\xd8":
|
||||||
image_data = image
|
image_data = image
|
||||||
else:
|
else:
|
||||||
with open(image, 'rb') as f:
|
with open(image, "rb") as f:
|
||||||
image_data = f.read()
|
image_data = f.read()
|
||||||
output_file = True
|
output_file = True
|
||||||
segments = split_into_segments(image_data)
|
segments = split_into_segments(image_data)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,26 +17,33 @@ def split(data):
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size:
|
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size:
|
||||||
fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH]
|
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
|
||||||
pointer += CHUNK_FOURCC_LENGTH
|
pointer += CHUNK_FOURCC_LENGTH
|
||||||
chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH]
|
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
|
||||||
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
||||||
pointer += LENGTH_BYTES_LENGTH
|
pointer += LENGTH_BYTES_LENGTH
|
||||||
|
|
||||||
chunk_data = data[pointer:pointer + chunk_length]
|
chunk_data = data[pointer : pointer + chunk_length]
|
||||||
chunks.append({"fourcc":fourcc, "length_bytes":chunk_length_bytes, "data":chunk_data})
|
chunks.append(
|
||||||
|
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
|
||||||
|
)
|
||||||
|
|
||||||
padding = 1 if chunk_length % 2 else 0
|
padding = 1 if chunk_length % 2 else 0
|
||||||
|
|
||||||
pointer += chunk_length + padding
|
pointer += chunk_length + padding
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def merge_chunks(chunks):
|
def merge_chunks(chunks):
|
||||||
merged = b"".join([chunk["fourcc"]
|
merged = b"".join(
|
||||||
+ chunk["length_bytes"]
|
[
|
||||||
+ chunk["data"]
|
chunk["fourcc"]
|
||||||
+ (len(chunk["data"]) % 2) * b"\x00"
|
+ chunk["length_bytes"]
|
||||||
for chunk in chunks])
|
+ chunk["data"]
|
||||||
|
+ (len(chunk["data"]) % 2) * b"\x00"
|
||||||
|
for chunk in chunks
|
||||||
|
]
|
||||||
|
)
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,6 +56,7 @@ def _get_size_from_vp8x(chunk):
|
||||||
height = height_minus_one + 1
|
height = height_minus_one + 1
|
||||||
return (width, height)
|
return (width, height)
|
||||||
|
|
||||||
|
|
||||||
def _get_size_from_vp8(chunk):
|
def _get_size_from_vp8(chunk):
|
||||||
BEGIN_CODE = b"\x9d\x01\x2a"
|
BEGIN_CODE = b"\x9d\x01\x2a"
|
||||||
begin_index = chunk["data"].find(BEGIN_CODE)
|
begin_index = chunk["data"].find(BEGIN_CODE)
|
||||||
|
|
@ -59,17 +66,21 @@ def _get_size_from_vp8(chunk):
|
||||||
BEGIN_CODE_LENGTH = len(BEGIN_CODE)
|
BEGIN_CODE_LENGTH = len(BEGIN_CODE)
|
||||||
LENGTH_BYTES_LENGTH = 2
|
LENGTH_BYTES_LENGTH = 2
|
||||||
length_start = begin_index + BEGIN_CODE_LENGTH
|
length_start = begin_index + BEGIN_CODE_LENGTH
|
||||||
width_bytes = chunk["data"][length_start:length_start + LENGTH_BYTES_LENGTH]
|
width_bytes = chunk["data"][length_start : length_start + LENGTH_BYTES_LENGTH]
|
||||||
width = struct.unpack("<H", width_bytes)[0]
|
width = struct.unpack("<H", width_bytes)[0]
|
||||||
height_bytes = chunk["data"][length_start + LENGTH_BYTES_LENGTH:length_start + 2 * LENGTH_BYTES_LENGTH]
|
height_bytes = chunk["data"][
|
||||||
|
length_start + LENGTH_BYTES_LENGTH : length_start + 2 * LENGTH_BYTES_LENGTH
|
||||||
|
]
|
||||||
height = struct.unpack("<H", height_bytes)[0]
|
height = struct.unpack("<H", height_bytes)[0]
|
||||||
return (width, height)
|
return (width, height)
|
||||||
|
|
||||||
|
|
||||||
def _vp8L_contains_alpha(chunk_data):
|
def _vp8L_contains_alpha(chunk_data):
|
||||||
flag = ord(chunk_data[4:5]) >> 5-1 & ord(b"\x01")
|
flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01")
|
||||||
contains = 1 * flag
|
contains = 1 * flag
|
||||||
return contains
|
return contains
|
||||||
|
|
||||||
|
|
||||||
def _get_size_from_vp8L(chunk):
|
def _get_size_from_vp8L(chunk):
|
||||||
b1 = chunk["data"][1:2]
|
b1 = chunk["data"][1:2]
|
||||||
b2 = chunk["data"][2:3]
|
b2 = chunk["data"][2:3]
|
||||||
|
|
@ -79,11 +90,14 @@ def _get_size_from_vp8L(chunk):
|
||||||
width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
|
width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
|
||||||
width = width_minus_one + 1
|
width = width_minus_one + 1
|
||||||
|
|
||||||
height_minus_one = (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
|
height_minus_one = (
|
||||||
|
(ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
|
||||||
|
)
|
||||||
height = height_minus_one + 1
|
height = height_minus_one + 1
|
||||||
|
|
||||||
return (width, height)
|
return (width, height)
|
||||||
|
|
||||||
|
|
||||||
def _get_size_from_anmf(chunk):
|
def _get_size_from_anmf(chunk):
|
||||||
width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
|
width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
|
||||||
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
|
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
|
||||||
|
|
@ -93,11 +107,21 @@ def _get_size_from_anmf(chunk):
|
||||||
height = height_minus_one + 1
|
height = height_minus_one + 1
|
||||||
return (width, height)
|
return (width, height)
|
||||||
|
|
||||||
|
|
||||||
def set_vp8x(chunks):
|
def set_vp8x(chunks):
|
||||||
|
|
||||||
width = None
|
width = None
|
||||||
height = None
|
height = None
|
||||||
flags = [b"0", b"0", b"0", b"0", b"0", b"0", b"0", b"0"] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0]
|
flags = [
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
b"0",
|
||||||
|
] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0]
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
if chunk["fourcc"] == b"VP8X":
|
if chunk["fourcc"] == b"VP8X":
|
||||||
|
|
@ -136,11 +160,16 @@ def set_vp8x(chunks):
|
||||||
|
|
||||||
data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes
|
data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes
|
||||||
|
|
||||||
vp8x_chunk = {"fourcc":header_bytes, "length_bytes":length_bytes, "data":data_bytes}
|
vp8x_chunk = {
|
||||||
|
"fourcc": header_bytes,
|
||||||
|
"length_bytes": length_bytes,
|
||||||
|
"data": data_bytes,
|
||||||
|
}
|
||||||
chunks.insert(0, vp8x_chunk)
|
chunks.insert(0, vp8x_chunk)
|
||||||
|
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def get_file_header(chunks):
|
def get_file_header(chunks):
|
||||||
WEBP_HEADER_LENGTH = 4
|
WEBP_HEADER_LENGTH = 4
|
||||||
FOURCC_LENGTH = 4
|
FOURCC_LENGTH = 4
|
||||||
|
|
@ -158,7 +187,6 @@ def get_file_header(chunks):
|
||||||
return file_header
|
return file_header
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_exif(data):
|
def get_exif(data):
|
||||||
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
|
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
|
||||||
raise ValueError("Not WebP")
|
raise ValueError("Not WebP")
|
||||||
|
|
@ -179,15 +207,15 @@ def get_exif(data):
|
||||||
chunks = []
|
chunks = []
|
||||||
exif = b""
|
exif = b""
|
||||||
while pointer < file_size:
|
while pointer < file_size:
|
||||||
fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH]
|
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
|
||||||
pointer += CHUNK_FOURCC_LENGTH
|
pointer += CHUNK_FOURCC_LENGTH
|
||||||
chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH]
|
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
|
||||||
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
||||||
if chunk_length % 2:
|
if chunk_length % 2:
|
||||||
chunk_length += 1
|
chunk_length += 1
|
||||||
pointer += LENGTH_BYTES_LENGTH
|
pointer += LENGTH_BYTES_LENGTH
|
||||||
if fourcc == b"EXIF":
|
if fourcc == b"EXIF":
|
||||||
return data[pointer:pointer + chunk_length]
|
return data[pointer : pointer + chunk_length]
|
||||||
pointer += chunk_length
|
pointer += chunk_length
|
||||||
return None # if there isn't exif, return None.
|
return None # if there isn't exif, return None.
|
||||||
|
|
||||||
|
|
@ -195,7 +223,11 @@ def get_exif(data):
|
||||||
def insert_exif_into_chunks(chunks, exif_bytes):
|
def insert_exif_into_chunks(chunks, exif_bytes):
|
||||||
EXIF_HEADER = b"EXIF"
|
EXIF_HEADER = b"EXIF"
|
||||||
exif_length_bytes = struct.pack("<L", len(exif_bytes))
|
exif_length_bytes = struct.pack("<L", len(exif_bytes))
|
||||||
exif_chunk = {"fourcc":EXIF_HEADER, "length_bytes":exif_length_bytes, "data":exif_bytes}
|
exif_chunk = {
|
||||||
|
"fourcc": EXIF_HEADER,
|
||||||
|
"length_bytes": exif_length_bytes,
|
||||||
|
"data": exif_bytes,
|
||||||
|
}
|
||||||
|
|
||||||
xmp_index = None
|
xmp_index = None
|
||||||
animation_index = None
|
animation_index = None
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,26 @@ class UserComment:
|
||||||
#
|
#
|
||||||
# Names of encodings that we publicly support.
|
# Names of encodings that we publicly support.
|
||||||
#
|
#
|
||||||
ASCII = 'ascii'
|
ASCII = "ascii"
|
||||||
JIS = 'jis'
|
JIS = "jis"
|
||||||
UNICODE = 'unicode'
|
UNICODE = "unicode"
|
||||||
ENCODINGS = (ASCII, JIS, UNICODE)
|
ENCODINGS = (ASCII, JIS, UNICODE)
|
||||||
|
|
||||||
#
|
#
|
||||||
# The actual encodings accepted by the standard library differ slightly from
|
# The actual encodings accepted by the standard library differ slightly from
|
||||||
# the above.
|
# the above.
|
||||||
#
|
#
|
||||||
_JIS = 'shift_jis'
|
_JIS = "shift_jis"
|
||||||
_UNICODE = 'utf_16_be'
|
_UNICODE = "utf_16_be"
|
||||||
|
|
||||||
_PREFIX_SIZE = 8
|
_PREFIX_SIZE = 8
|
||||||
#
|
#
|
||||||
# From Table 9: Character Codes and their Designation
|
# From Table 9: Character Codes and their Designation
|
||||||
#
|
#
|
||||||
_ASCII_PREFIX = b'\x41\x53\x43\x49\x49\x00\x00\x00'
|
_ASCII_PREFIX = b"\x41\x53\x43\x49\x49\x00\x00\x00"
|
||||||
_JIS_PREFIX = b'\x4a\x49\x53\x00\x00\x00\x00\x00'
|
_JIS_PREFIX = b"\x4a\x49\x53\x00\x00\x00\x00\x00"
|
||||||
_UNICODE_PREFIX = b'\x55\x4e\x49\x43\x4f\x44\x45\x00'
|
_UNICODE_PREFIX = b"\x55\x4e\x49\x43\x4f\x44\x45\x00"
|
||||||
_UNDEFINED_PREFIX = b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
_UNDEFINED_PREFIX = b"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, data):
|
def load(cls, data):
|
||||||
|
|
@ -35,18 +35,20 @@ class UserComment:
|
||||||
or the encoding is unsupported.
|
or the encoding is unsupported.
|
||||||
"""
|
"""
|
||||||
if len(data) < cls._PREFIX_SIZE:
|
if len(data) < cls._PREFIX_SIZE:
|
||||||
raise ValueError('not enough data to decode UserComment')
|
raise ValueError("not enough data to decode UserComment")
|
||||||
prefix = data[:cls._PREFIX_SIZE]
|
prefix = data[: cls._PREFIX_SIZE]
|
||||||
body = data[cls._PREFIX_SIZE:]
|
body = data[cls._PREFIX_SIZE :]
|
||||||
if prefix == cls._UNDEFINED_PREFIX:
|
if prefix == cls._UNDEFINED_PREFIX:
|
||||||
raise ValueError('prefix is UNDEFINED, unable to decode UserComment')
|
raise ValueError("prefix is UNDEFINED, unable to decode UserComment")
|
||||||
try:
|
try:
|
||||||
encoding = {
|
encoding = {
|
||||||
cls._ASCII_PREFIX: cls.ASCII, cls._JIS_PREFIX: cls._JIS, cls._UNICODE_PREFIX: cls._UNICODE,
|
cls._ASCII_PREFIX: cls.ASCII,
|
||||||
|
cls._JIS_PREFIX: cls._JIS,
|
||||||
|
cls._UNICODE_PREFIX: cls._UNICODE,
|
||||||
}[prefix]
|
}[prefix]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError('unable to determine appropriate encoding')
|
raise ValueError("unable to determine appropriate encoding")
|
||||||
return body.decode(encoding, errors='replace')
|
return body.decode(encoding, errors="replace")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dump(cls, data, encoding="ascii"):
|
def dump(cls, data, encoding="ascii"):
|
||||||
|
|
@ -60,7 +62,15 @@ class UserComment:
|
||||||
:raises: ValueError if the encoding is unsupported.
|
:raises: ValueError if the encoding is unsupported.
|
||||||
"""
|
"""
|
||||||
if encoding not in cls.ENCODINGS:
|
if encoding not in cls.ENCODINGS:
|
||||||
raise ValueError('encoding %r must be one of %r' % (encoding, cls.ENCODINGS))
|
raise ValueError(
|
||||||
prefix = {cls.ASCII: cls._ASCII_PREFIX, cls.JIS: cls._JIS_PREFIX, cls.UNICODE: cls._UNICODE_PREFIX}[encoding]
|
"encoding %r must be one of %r" % (encoding, cls.ENCODINGS)
|
||||||
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(encoding, encoding)
|
)
|
||||||
return prefix + data.encode(internal_encoding, errors='replace')
|
prefix = {
|
||||||
|
cls.ASCII: cls._ASCII_PREFIX,
|
||||||
|
cls.JIS: cls._JIS_PREFIX,
|
||||||
|
cls.UNICODE: cls._UNICODE_PREFIX,
|
||||||
|
}[encoding]
|
||||||
|
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(
|
||||||
|
encoding, encoding
|
||||||
|
)
|
||||||
|
return prefix + data.encode(internal_encoding, errors="replace")
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,12 @@ def set_gain(camera, gain, value):
|
||||||
if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]:
|
if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]:
|
||||||
raise ValueError("The gain parameter was not valid")
|
raise ValueError("The gain parameter was not valid")
|
||||||
ret = mmal.mmal_port_parameter_set_rational(
|
ret = mmal.mmal_port_parameter_set_rational(
|
||||||
camera._camera.control._port,
|
camera._camera.control._port, gain, to_rational(value)
|
||||||
gain,
|
|
||||||
to_rational(value)
|
|
||||||
)
|
)
|
||||||
if ret == 4:
|
if ret == 4:
|
||||||
raise exc.PiCameraMMALError(
|
raise exc.PiCameraMMALError(
|
||||||
ret,
|
ret,
|
||||||
"Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017."
|
"Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017.",
|
||||||
)
|
)
|
||||||
elif ret != 0:
|
elif ret != 0:
|
||||||
raise exc.PiCameraMMALError(ret)
|
raise exc.PiCameraMMALError(ret)
|
||||||
|
|
@ -56,7 +54,9 @@ if __name__ == "__main__":
|
||||||
# fix the shutter speed
|
# fix the shutter speed
|
||||||
cam.shutter_speed = cam.exposure_speed
|
cam.shutter_speed = cam.exposure_speed
|
||||||
|
|
||||||
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
|
logging.info(
|
||||||
|
"Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
|
||||||
|
)
|
||||||
|
|
||||||
logging.info("Attempting to set analogue gain to 1")
|
logging.info("Attempting to set analogue gain to 1")
|
||||||
set_analog_gain(cam, 1)
|
set_analog_gain(cam, 1)
|
||||||
|
|
@ -70,7 +70,11 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
logging.info("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
|
logging.info(
|
||||||
|
"Current a/d gains: {}, {}".format(
|
||||||
|
cam.analog_gain, cam.digital_gain
|
||||||
|
)
|
||||||
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.info("Stopping...")
|
logging.info("Stopping...")
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ def convert_type_to_json_safe(v):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def settings_to_json(data: dict):
|
def settings_to_json(data: dict):
|
||||||
"""
|
"""
|
||||||
Make a copy of an input dictionary that's safe for JSON return
|
Make a copy of an input dictionary that's safe for JSON return
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ class TaskDeniedException(Exception):
|
||||||
|
|
||||||
class LockError(ThreadError):
|
class LockError(ThreadError):
|
||||||
ERROR_CODES = {
|
ERROR_CODES = {
|
||||||
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.",
|
"ACQUIRE_ERROR": "Unable to acquire. Lock in use by another thread.",
|
||||||
'IN_USE_ERROR': "Lock in use by another thread."
|
"IN_USE_ERROR": "Lock in use by another thread.",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, code, lock):
|
def __init__(self, code, lock):
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class StrictLock(object):
|
||||||
_lock (:py:class:`threading.RLock`): Parent RLock object
|
_lock (:py:class:`threading.RLock`): Parent RLock object
|
||||||
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
|
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, timeout=1):
|
def __init__(self, timeout=1):
|
||||||
self._lock = RLock()
|
self._lock = RLock()
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
@ -29,7 +30,7 @@ class StrictLock(object):
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
raise LockError('ACQUIRE_ERROR', self)
|
raise LockError("ACQUIRE_ERROR", self)
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|
@ -51,6 +52,7 @@ class CompositeLock(object):
|
||||||
locks (list): List of parent RLock objects
|
locks (list): List of parent RLock objects
|
||||||
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
|
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, locks, timeout=1):
|
def __init__(self, locks, timeout=1):
|
||||||
self.locks = locks
|
self.locks = locks
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
@ -63,7 +65,7 @@ class CompositeLock(object):
|
||||||
if all(result):
|
if all(result):
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
raise LockError('ACQUIRE_ERROR', self)
|
raise LockError("ACQUIRE_ERROR", self)
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
for lock in self.locks:
|
for lock in self.locks:
|
||||||
|
|
|
||||||
|
|
@ -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
|
from .plugin import AutofocusPlugin
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify
|
from openflexure_microscope.devel import (
|
||||||
|
MicroscopeViewPlugin,
|
||||||
|
JsonResponse,
|
||||||
|
request,
|
||||||
|
jsonify,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MeasureSharpnessAPI(MicroscopeViewPlugin):
|
class MeasureSharpnessAPI(MicroscopeViewPlugin):
|
||||||
def post(self):
|
def post(self):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
return jsonify({'sharpness': self.plugin.measure_sharpness()})
|
return jsonify({"sharpness": self.plugin.measure_sharpness()})
|
||||||
|
|
||||||
|
|
||||||
class AutofocusAPI(MicroscopeViewPlugin):
|
class AutofocusAPI(MicroscopeViewPlugin):
|
||||||
|
|
@ -22,6 +28,7 @@ class AutofocusAPI(MicroscopeViewPlugin):
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 202
|
||||||
|
|
||||||
|
|
||||||
class FastAutofocusAPI(MicroscopeViewPlugin):
|
class FastAutofocusAPI(MicroscopeViewPlugin):
|
||||||
def post(self):
|
def post(self):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
@ -33,7 +40,9 @@ class FastAutofocusAPI(MicroscopeViewPlugin):
|
||||||
backlash = 0
|
backlash = 0
|
||||||
|
|
||||||
logging.info("Running autofocus...")
|
logging.info("Running autofocus...")
|
||||||
task = self.microscope.task.start(self.plugin.fast_autofocus, dz, backlash=backlash)
|
task = self.microscope.task.start(
|
||||||
|
self.plugin.fast_autofocus, dz, backlash=backlash
|
||||||
|
)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 202
|
||||||
|
|
@ -4,7 +4,8 @@ import threading
|
||||||
import logging
|
import logging
|
||||||
from scipy import ndimage
|
from scipy import ndimage
|
||||||
|
|
||||||
class JPEGSharpnessMonitor():
|
|
||||||
|
class JPEGSharpnessMonitor:
|
||||||
def __init__(self, microscope, timeout=60):
|
def __init__(self, microscope, timeout=60):
|
||||||
self.microscope = microscope
|
self.microscope = microscope
|
||||||
self.camera = microscope.camera
|
self.camera = microscope.camera
|
||||||
|
|
@ -26,10 +27,12 @@ class JPEGSharpnessMonitor():
|
||||||
|
|
||||||
def should_stop(self):
|
def should_stop(self):
|
||||||
import time
|
import time
|
||||||
|
|
||||||
return time.time() - self.kept_alive > self.timeout
|
return time.time() - self.kept_alive > self.timeout
|
||||||
|
|
||||||
def keep_alive(self):
|
def keep_alive(self):
|
||||||
import time
|
import time
|
||||||
|
|
||||||
self.kept_alive = time.time()
|
self.kept_alive = time.time()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
|
@ -59,7 +62,6 @@ class JPEGSharpnessMonitor():
|
||||||
"""Return the size of a frame from the MJPEG stream"""
|
"""Return the size of a frame from the MJPEG stream"""
|
||||||
return len(self.camera.get_frame())
|
return len(self.camera.get_frame())
|
||||||
|
|
||||||
|
|
||||||
def focus_rel(self, dz, backlash=False, **kwargs):
|
def focus_rel(self, dz, backlash=False, **kwargs):
|
||||||
self.keep_alive()
|
self.keep_alive()
|
||||||
self.stage_times.append(time.time())
|
self.stage_times.append(time.time())
|
||||||
|
|
@ -96,14 +98,17 @@ class JPEGSharpnessMonitor():
|
||||||
def data_dict(self):
|
def data_dict(self):
|
||||||
"""Return the gathered data as a single convenient dictionary"""
|
"""Return the gathered data as a single convenient dictionary"""
|
||||||
data = {}
|
data = {}
|
||||||
for k in ['jpeg_times', 'jpeg_sizes', 'stage_times', 'stage_positions']:
|
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
|
||||||
data[k] = getattr(self, k)
|
data[k] = getattr(self, k)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def decimate_to(shape, image):
|
def decimate_to(shape, image):
|
||||||
"""Decimate an image to reduce its size if it's too big."""
|
"""Decimate an image to reduce its size if it's too big."""
|
||||||
decimation = np.max(np.ceil(np.array(image.shape, dtype=np.float)[:len(shape)]/np.array(shape)))
|
decimation = np.max(
|
||||||
return image[::int(decimation), ::int(decimation), ...]
|
np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape))
|
||||||
|
)
|
||||||
|
return image[:: int(decimation), :: int(decimation), ...]
|
||||||
|
|
||||||
|
|
||||||
def sharpness_sum_lap2(rgb_image):
|
def sharpness_sum_lap2(rgb_image):
|
||||||
|
|
@ -111,12 +116,14 @@ def sharpness_sum_lap2(rgb_image):
|
||||||
# image_bw=np.mean(decimate_to((1000,1000), rgb_image),2)
|
# image_bw=np.mean(decimate_to((1000,1000), rgb_image),2)
|
||||||
image_bw = np.mean(rgb_image, 2)
|
image_bw = np.mean(rgb_image, 2)
|
||||||
image_lap = ndimage.filters.laplace(image_bw)
|
image_lap = ndimage.filters.laplace(image_bw)
|
||||||
return np.mean(image_lap.astype(np.float)**4)
|
return np.mean(image_lap.astype(np.float) ** 4)
|
||||||
|
|
||||||
|
|
||||||
def sharpness_edge(image):
|
def sharpness_edge(image):
|
||||||
"""Return a sharpness metric optimised for vertical lines"""
|
"""Return a sharpness metric optimised for vertical lines"""
|
||||||
gray = np.mean(image.astype(float), 2)
|
gray = np.mean(image.astype(float), 2)
|
||||||
n = 20
|
n = 20
|
||||||
edge = np.array([[-1]*n + [1]*n])
|
edge = np.array([[-1] * n + [1] * n])
|
||||||
return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]])
|
return np.sum(
|
||||||
|
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,16 @@ from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI
|
||||||
|
|
||||||
from openflexure_microscope.devel import MicroscopePlugin
|
from openflexure_microscope.devel import MicroscopePlugin
|
||||||
|
|
||||||
|
|
||||||
class AutofocusPlugin(MicroscopePlugin):
|
class AutofocusPlugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
Basic autofocus plugin
|
Basic autofocus plugin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_views = {
|
api_views = {
|
||||||
'/measure_sharpness': MeasureSharpnessAPI,
|
"/measure_sharpness": MeasureSharpnessAPI,
|
||||||
'/autofocus': AutofocusAPI,
|
"/autofocus": AutofocusAPI,
|
||||||
'/fast_autofocus': FastAutofocusAPI,
|
"/fast_autofocus": FastAutofocusAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
### SLOW AUTOFOCUS
|
### SLOW AUTOFOCUS
|
||||||
|
|
@ -70,21 +71,24 @@ class AutofocusPlugin(MicroscopePlugin):
|
||||||
m.focus_rel(dz)
|
m.focus_rel(dz)
|
||||||
return m.sharpest_z_on_move(0)
|
return m.sharpest_z_on_move(0)
|
||||||
|
|
||||||
|
|
||||||
def fast_autofocus(self, dz=2000, backlash=None):
|
def fast_autofocus(self, dz=2000, backlash=None):
|
||||||
"""Perform a down-up-down-up autofocus"""
|
"""Perform a down-up-down-up autofocus"""
|
||||||
with self.monitor_sharpness() as m:
|
with self.monitor_sharpness() as m:
|
||||||
i, z = m.focus_rel(-dz/2)
|
i, z = m.focus_rel(-dz / 2)
|
||||||
i, z = m.focus_rel(dz)
|
i, z = m.focus_rel(dz)
|
||||||
fz = m.sharpest_z_on_move(i)
|
fz = m.sharpest_z_on_move(i)
|
||||||
if backlash is None:
|
if backlash is None:
|
||||||
i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent
|
i, z = m.focus_rel(
|
||||||
|
-dz
|
||||||
|
) # move all the way to the start so it's consistent
|
||||||
else:
|
else:
|
||||||
i, z = m.focus_rel(fz - z - backlash)
|
i, z = m.focus_rel(fz - z - backlash)
|
||||||
m.focus_rel(fz - z)
|
m.focus_rel(fz - z)
|
||||||
return m.data_dict()
|
return m.data_dict()
|
||||||
|
|
||||||
def fast_up_down_up_autofocus(self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150):
|
def fast_up_down_up_autofocus(
|
||||||
|
self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150
|
||||||
|
):
|
||||||
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
||||||
|
|
||||||
This autofocus method is very efficient, as it only passes the peak once.
|
This autofocus method is very efficient, as it only passes the peak once.
|
||||||
|
|
@ -124,32 +128,41 @@ class AutofocusPlugin(MicroscopePlugin):
|
||||||
# Ensure the MJPEG stream has started
|
# Ensure the MJPEG stream has started
|
||||||
self.microscope.camera.start_stream_recording()
|
self.microscope.camera.start_stream_recording()
|
||||||
|
|
||||||
df = dz #TODO: refactor so I actually use dz in the code below!
|
df = dz # TODO: refactor so I actually use dz in the code below!
|
||||||
if initial_move_up:
|
if initial_move_up:
|
||||||
m.focus_rel(df/2)
|
m.focus_rel(df / 2)
|
||||||
# move down
|
# move down
|
||||||
i, z = m.focus_rel(-df)
|
i, z = m.focus_rel(-df)
|
||||||
# now inspect where the sharpest point is, and estimate the sharpness
|
# now inspect where the sharpest point is, and estimate the sharpness
|
||||||
# (JPEG size) that we should find at the start of the Z stack
|
# (JPEG size) that we should find at the start of the Z stack
|
||||||
jt, jz, js = m.move_data(i)
|
jt, jz, js = m.move_data(i)
|
||||||
best_z = jz[np.argmax(js)]
|
best_z = jz[np.argmax(js)]
|
||||||
target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing
|
target_s = np.interp(
|
||||||
|
[best_z + target_z], jz[::-1], js[::-1]
|
||||||
|
) # NB jz is decreasing
|
||||||
|
|
||||||
# now move to the start of the z stack
|
# now move to the start of the z stack
|
||||||
i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack
|
i, z = m.focus_rel(
|
||||||
|
best_z + target_z - z + mini_backlash
|
||||||
|
) # takes us to the start of the stack
|
||||||
|
|
||||||
# We've deliberately undershot - figure out how much further we should move based on the curve
|
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||||
current_js = m.jpeg_size()
|
current_js = m.jpeg_size()
|
||||||
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
||||||
js = js[imax:] # NB z is in DECREASING order
|
js = js[imax:] # NB z is in DECREASING order
|
||||||
jz = jz[imax:]
|
jz = jz[imax:]
|
||||||
inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position
|
inow = np.argmax(
|
||||||
|
js < current_js
|
||||||
|
) # use the curve we recorded to estimate our position
|
||||||
# TODO: fancy interpolation stuff
|
# TODO: fancy interpolation stuff
|
||||||
|
|
||||||
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
||||||
# That means we should move forwards, by best_z - zs[inow]
|
# That means we should move forwards, by best_z - zs[inow]
|
||||||
correction_move = best_z + target_z - jz[inow]
|
correction_move = best_z + target_z - jz[inow]
|
||||||
logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move))
|
logging.debug(
|
||||||
|
"Fast autofocus scan: correcting backlash by moving {} steps".format(
|
||||||
|
correction_move
|
||||||
|
)
|
||||||
|
)
|
||||||
m.focus_rel(correction_move)
|
m.focus_rel(correction_move)
|
||||||
return m.data_dict()
|
return m.data_dict()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__all__ = ['Plugin']
|
__all__ = ["Plugin"]
|
||||||
from .plugin import 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
|
import logging
|
||||||
|
|
||||||
|
|
@ -19,9 +25,7 @@ class Plugin(MicroscopePlugin):
|
||||||
A set of default plugins
|
A set of default plugins
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_views = {
|
api_views = {"/recalibrate": RecalibrateAPIView}
|
||||||
'/recalibrate': RecalibrateAPIView,
|
|
||||||
}
|
|
||||||
|
|
||||||
def recalibrate(self):
|
def recalibrate(self):
|
||||||
"""Reset the camera's settings.
|
"""Reset the camera's settings.
|
||||||
|
|
@ -32,8 +36,10 @@ class Plugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
scamera = self.microscope.camera
|
scamera = self.microscope.camera
|
||||||
with scamera.lock:
|
with scamera.lock:
|
||||||
assert not scamera.state['record_active'], "Can't recalibrate while recording!"
|
assert not scamera.state[
|
||||||
streaming = scamera.state['stream_active']
|
"record_active"
|
||||||
|
], "Can't recalibrate while recording!"
|
||||||
|
streaming = scamera.state["stream_active"]
|
||||||
if streaming:
|
if streaming:
|
||||||
logging.info("Stopping stream before recalibration")
|
logging.info("Stopping stream before recalibration")
|
||||||
scamera.stop_stream_recording(resolution=(640, 480))
|
scamera.stop_stream_recording(resolution=(640, 480))
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import time
|
||||||
from picamera import PiCamera
|
from picamera import PiCamera
|
||||||
from picamera.array import PiRGBArray, PiBayerArray
|
from picamera.array import PiRGBArray, PiBayerArray
|
||||||
|
|
||||||
|
|
||||||
def rgb_image(camera, resize=None, **kwargs):
|
def rgb_image(camera, resize=None, **kwargs):
|
||||||
"""Capture an image and return an RGB numpy array"""
|
"""Capture an image and return an RGB numpy array"""
|
||||||
with PiRGBArray(camera, size=resize) as output:
|
with PiRGBArray(camera, size=resize) as output:
|
||||||
camera.capture(output, format='rgb', resize=resize, **kwargs)
|
camera.capture(output, format="rgb", resize=resize, **kwargs)
|
||||||
return output.array
|
return output.array
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,7 +20,9 @@ def flat_lens_shading_table(camera):
|
||||||
library (with lens shading table support) it will raise an error.
|
library (with lens shading table support) it will raise an error.
|
||||||
"""
|
"""
|
||||||
if not hasattr(PiCamera, "lens_shading_table"):
|
if not hasattr(PiCamera, "lens_shading_table"):
|
||||||
raise ImportError("This program requires the forked picamera library with lens shading support")
|
raise ImportError(
|
||||||
|
"This program requires the forked picamera library with lens shading support"
|
||||||
|
)
|
||||||
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
|
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -28,7 +31,9 @@ def adjust_exposure_to_setpoint(camera, setpoint):
|
||||||
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
camera.shutter_speed = int(camera.shutter_speed * setpoint / np.max(rgb_image(camera)))
|
camera.shutter_speed = int(
|
||||||
|
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
|
||||||
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
|
|
@ -38,7 +43,9 @@ def auto_expose_and_freeze_settings(camera):
|
||||||
print("Allowing the camera to auto-expose")
|
print("Allowing the camera to auto-expose")
|
||||||
camera.awb_mode = "auto"
|
camera.awb_mode = "auto"
|
||||||
camera.exposure_mode = "auto"
|
camera.exposure_mode = "auto"
|
||||||
camera.iso = 0 # This is important, if it's on a fixed ISO, gain might not set properly.
|
camera.iso = (
|
||||||
|
0
|
||||||
|
) # This is important, if it's on a fixed ISO, gain might not set properly.
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
@ -53,17 +60,26 @@ def auto_expose_and_freeze_settings(camera):
|
||||||
camera.awb_mode = "off"
|
camera.awb_mode = "off"
|
||||||
camera.awb_gains = g
|
camera.awb_gains = g
|
||||||
print("Auto white balance disabled, gains are {}".format(g))
|
print("Auto white balance disabled, gains are {}".format(g))
|
||||||
print("Analogue gain: {}, Digital gain: {}".format(camera.analog_gain, camera.digital_gain))
|
print(
|
||||||
|
"Analogue gain: {}, Digital gain: {}".format(
|
||||||
|
camera.analog_gain, camera.digital_gain
|
||||||
|
)
|
||||||
|
)
|
||||||
adjust_exposure_to_setpoint(camera, 215)
|
adjust_exposure_to_setpoint(camera, 215)
|
||||||
|
|
||||||
|
|
||||||
def channels_from_bayer_array(bayer_array):
|
def channels_from_bayer_array(bayer_array):
|
||||||
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
||||||
bayer_pattern = [(i//2, i % 2) for i in range(4)]
|
bayer_pattern = [(i // 2, i % 2) for i in range(4)]
|
||||||
channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype)
|
channels = np.zeros(
|
||||||
|
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
|
||||||
|
dtype=bayer_array.dtype,
|
||||||
|
)
|
||||||
for i, offset in enumerate(bayer_pattern):
|
for i, offset in enumerate(bayer_pattern):
|
||||||
# We simplify life by dealing with only one channel at a time.
|
# We simplify life by dealing with only one channel at a time.
|
||||||
channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2)
|
channels[i, :, :] = np.sum(
|
||||||
|
bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2
|
||||||
|
)
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
|
@ -88,23 +104,25 @@ def lst_from_channels(channels):
|
||||||
# half the size of the full image - remember the Bayer pattern... This
|
# half the size of the full image - remember the Bayer pattern... This
|
||||||
# should give results very close to 6by9's solution, albeit considerably
|
# should give results very close to 6by9's solution, albeit considerably
|
||||||
# less computationally efficient!
|
# less computationally efficient!
|
||||||
padded_image_channel = np.pad(image_channel,
|
padded_image_channel = np.pad(
|
||||||
[(0, lw*32 - iw), (0, lh*32 - ih)],
|
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
|
||||||
mode="edge") # Pad image to the right and bottom
|
) # Pad image to the right and bottom
|
||||||
print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw,
|
print(
|
||||||
ih,
|
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
|
||||||
lw*32,
|
iw, ih, lw * 32, lh * 32, padded_image_channel.shape
|
||||||
lh*32,
|
)
|
||||||
padded_image_channel.shape))
|
)
|
||||||
# Next, fill the shading table (except edge pixels). Please excuse the
|
# Next, fill the shading table (except edge pixels). Please excuse the
|
||||||
# for loop - I know it's not fast but this code needn't be!
|
# for loop - I know it's not fast but this code needn't be!
|
||||||
box = 3 # We average together a square of this side length for each pixel.
|
box = 3 # We average together a square of this side length for each pixel.
|
||||||
# NB this isn't quite what 6by9's program does - it averages 3 pixels
|
# NB this isn't quite what 6by9's program does - it averages 3 pixels
|
||||||
# horizontally, but not vertically.
|
# horizontally, but not vertically.
|
||||||
for dx in np.arange(box) - box//2:
|
for dx in np.arange(box) - box // 2:
|
||||||
for dy in np.arange(box) - box//2:
|
for dy in np.arange(box) - box // 2:
|
||||||
ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64
|
ls_channel[:, :] += (
|
||||||
ls_channel /= box**2
|
padded_image_channel[16 + dx :: 32, 16 + dy :: 32] - 64
|
||||||
|
)
|
||||||
|
ls_channel /= box ** 2
|
||||||
# The original C code written by 6by9 normalises to the central 64 pixels in each channel.
|
# The original C code written by 6by9 normalises to the central 64 pixels in each channel.
|
||||||
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
|
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
|
||||||
# I have had better results just normalising to the maximum:
|
# I have had better results just normalising to the maximum:
|
||||||
|
|
@ -117,7 +135,7 @@ def lst_from_channels(channels):
|
||||||
|
|
||||||
# What we actually want to calculate is the gains needed to compensate for the
|
# What we actually want to calculate is the gains needed to compensate for the
|
||||||
# lens shading - that's 1/lens_shading_table_float as we currently have it.
|
# lens shading - that's 1/lens_shading_table_float as we currently have it.
|
||||||
gains = 32.0/lens_shading # 32 is unity gain
|
gains = 32.0 / lens_shading # 32 is unity gain
|
||||||
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
|
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
|
||||||
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
|
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
|
||||||
lens_shading_table = gains.astype(np.uint8)
|
lens_shading_table = gains.astype(np.uint8)
|
||||||
|
|
@ -154,8 +172,10 @@ def recalibrate_camera(camera):
|
||||||
# Fix the AWB gains so the image is neutral
|
# Fix the AWB gains so the image is neutral
|
||||||
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
|
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
|
||||||
old_gains = camera.awb_gains
|
old_gains = camera.awb_gains
|
||||||
camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0],
|
camera.awb_gains = (
|
||||||
channel_means[1]/channel_means[2]*old_gains[1])
|
channel_means[1] / channel_means[0] * old_gains[0],
|
||||||
|
channel_means[1] / channel_means[2] * old_gains[1],
|
||||||
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
# Ensure the background is bright but not saturated
|
# Ensure the background is bright but not saturated
|
||||||
adjust_exposure_to_setpoint(camera, 230)
|
adjust_exposure_to_setpoint(camera, 230)
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__all__ = ['ScanPlugin']
|
__all__ = ["ScanPlugin"]
|
||||||
from .plugin import 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
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class TileScanAPI(MicroscopeViewPlugin):
|
class TileScanAPI(MicroscopeViewPlugin):
|
||||||
def post(self):
|
def post(self):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
# Get params
|
# Get params
|
||||||
filename = payload.param('filename')
|
filename = payload.param("filename")
|
||||||
temporary = payload.param('temporary', default=False, convert=bool)
|
temporary = payload.param("temporary", default=False, convert=bool)
|
||||||
|
|
||||||
step_size = payload.param('step_size', default=[2000, 1500, 100], convert=list)
|
step_size = payload.param("step_size", default=[2000, 1500, 100], convert=list)
|
||||||
step_size = [int(i) for i in step_size]
|
step_size = [int(i) for i in step_size]
|
||||||
|
|
||||||
grid = payload.param('grid', default=[3, 3, 5], convert=list)
|
grid = payload.param("grid", default=[3, 3, 5], convert=list)
|
||||||
grid = [int(i) for i in grid]
|
grid = [int(i) for i in grid]
|
||||||
|
|
||||||
style = payload.param('style', default='raster', convert=str)
|
style = payload.param("style", default="raster", convert=str)
|
||||||
autofocus_dz = payload.param('autofocus_dz', default=50, convert=int)
|
autofocus_dz = payload.param("autofocus_dz", default=50, convert=int)
|
||||||
fast_autofocus = payload.param('fast_autofocus', default=False, convert=bool)
|
fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool)
|
||||||
|
|
||||||
use_video_port = payload.param('use_video_port', default=True, convert=bool)
|
use_video_port = payload.param("use_video_port", default=True, convert=bool)
|
||||||
resize = payload.param('size', default=None)
|
resize = payload.param("size", default=None)
|
||||||
if resize:
|
if resize:
|
||||||
if ('width' in resize) and ('height' in resize):
|
if ("width" in resize) and ("height" in resize):
|
||||||
resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple
|
resize = (
|
||||||
|
int(resize["width"]),
|
||||||
|
int(resize["height"]),
|
||||||
|
) # Convert dict to tuple
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
bayer = payload.param('bayer', default=False, convert=bool)
|
bayer = payload.param("bayer", default=False, convert=bool)
|
||||||
metadata = payload.param('metadata', default={}, convert=dict)
|
metadata = payload.param("metadata", default={}, convert=dict)
|
||||||
tags = payload.param('tags', default=[], convert=list)
|
tags = payload.param("tags", default=[], convert=list)
|
||||||
|
|
||||||
logging.info("Running tile scan...")
|
logging.info("Running tile scan...")
|
||||||
task = self.microscope.task.start(
|
task = self.microscope.task.start(
|
||||||
|
|
@ -46,7 +56,7 @@ class TileScanAPI(MicroscopeViewPlugin):
|
||||||
bayer=bayer,
|
bayer=bayer,
|
||||||
fast_autofocus=fast_autofocus,
|
fast_autofocus=fast_autofocus,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
tags=tags
|
tags=tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
|
|
|
||||||
|
|
@ -11,29 +11,31 @@ from openflexure_microscope.devel import MicroscopePlugin
|
||||||
|
|
||||||
from .api import TileScanAPI
|
from .api import TileScanAPI
|
||||||
|
|
||||||
def construct_grid(initial, step_sizes, n_steps, style='raster'):
|
|
||||||
|
def construct_grid(initial, step_sizes, n_steps, style="raster"):
|
||||||
"""
|
"""
|
||||||
Given an initial position, step sizes, and number of steps,
|
Given an initial position, step sizes, and number of steps,
|
||||||
construct a 2-dimensional list of scan x-y positions.
|
construct a 2-dimensional list of scan x-y positions.
|
||||||
"""
|
"""
|
||||||
arr = []
|
arr = []
|
||||||
|
|
||||||
for i in range(n_steps[0]): # x axis
|
for i in range(n_steps[0]): # x axis
|
||||||
arr.append([])
|
arr.append([])
|
||||||
for j in range(n_steps[1]): # y axis
|
for j in range(n_steps[1]): # y axis
|
||||||
# Create a coordinate array
|
# Create a coordinate array
|
||||||
coord = [initial[ax] + [i, j][ax]*step_sizes[ax] for ax in range(2)]
|
coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)]
|
||||||
# Append coordinate array to position grid
|
# Append coordinate array to position grid
|
||||||
arr[i].append(tuple(coord))
|
arr[i].append(tuple(coord))
|
||||||
|
|
||||||
# Style modifiers
|
# Style modifiers
|
||||||
if style == 'snake':
|
if style == "snake":
|
||||||
for i, line in enumerate(arr):
|
for i, line in enumerate(arr):
|
||||||
if i % 2 != 0:
|
if i % 2 != 0:
|
||||||
line.reverse()
|
line.reverse()
|
||||||
|
|
||||||
return arr
|
return arr
|
||||||
|
|
||||||
|
|
||||||
def flatten_grid(grid):
|
def flatten_grid(grid):
|
||||||
"""
|
"""
|
||||||
Convert a 3D list of scan positions into a flat list
|
Convert a 3D list of scan positions into a flat list
|
||||||
|
|
@ -43,24 +45,25 @@ def flatten_grid(grid):
|
||||||
grid = list(itertools.chain(*grid))
|
grid = list(itertools.chain(*grid))
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
|
|
||||||
class ScanPlugin(MicroscopePlugin):
|
class ScanPlugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
Stack and tile plugin
|
Stack and tile plugin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_views = {
|
api_views = {"/tile": TileScanAPI}
|
||||||
'/tile': TileScanAPI,
|
|
||||||
}
|
|
||||||
|
|
||||||
def capture(self,
|
def capture(
|
||||||
basename,
|
self,
|
||||||
scan_id,
|
basename,
|
||||||
temporary: bool = False,
|
scan_id,
|
||||||
use_video_port: bool = False,
|
temporary: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
use_video_port: bool = False,
|
||||||
bayer: bool = False,
|
resize: Tuple[int, int] = None,
|
||||||
metadata: dict = {},
|
bayer: bool = False,
|
||||||
tags: list = []):
|
metadata: dict = {},
|
||||||
|
tags: list = [],
|
||||||
|
):
|
||||||
|
|
||||||
# Construct a tile filename
|
# Construct a tile filename
|
||||||
filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position)
|
filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position)
|
||||||
|
|
@ -68,47 +71,46 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
|
|
||||||
# Create output object
|
# Create output object
|
||||||
output = self.microscope.camera.new_image(
|
output = self.microscope.camera.new_image(
|
||||||
write_to_file=True,
|
write_to_file=True, temporary=temporary, filename=filename, folder=folder
|
||||||
temporary=temporary,
|
)
|
||||||
filename=filename,
|
|
||||||
folder=folder)
|
|
||||||
|
|
||||||
# Capture
|
# Capture
|
||||||
self.microscope.camera.capture(
|
self.microscope.camera.capture(
|
||||||
output,
|
output, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||||
use_video_port=use_video_port,
|
)
|
||||||
resize=resize,
|
|
||||||
bayer=bayer)
|
|
||||||
|
|
||||||
# Affix metadata
|
# Affix metadata
|
||||||
if 'scan' not in tags:
|
if "scan" not in tags:
|
||||||
tags.append('scan')
|
tags.append("scan")
|
||||||
|
|
||||||
metadata.update({
|
metadata.update(
|
||||||
'position': self.microscope.state['stage']['position'],
|
{
|
||||||
'scan_id': scan_id,
|
"position": self.microscope.state["stage"]["position"],
|
||||||
'basename': basename,
|
"scan_id": scan_id,
|
||||||
'microscope_id': self.microscope.id,
|
"basename": basename,
|
||||||
'microscope_name': self.microscope.name
|
"microscope_id": self.microscope.id,
|
||||||
})
|
"microscope_name": self.microscope.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
output.put_metadata(metadata)
|
output.put_metadata(metadata)
|
||||||
output.put_tags(tags)
|
output.put_tags(tags)
|
||||||
|
|
||||||
def tile(
|
def tile(
|
||||||
self,
|
self,
|
||||||
basename: str = None,
|
basename: str = None,
|
||||||
temporary: bool = False,
|
temporary: bool = False,
|
||||||
step_size: int = [2000, 1500, 100],
|
step_size: int = [2000, 1500, 100],
|
||||||
grid: list = [3, 3, 5],
|
grid: list = [3, 3, 5],
|
||||||
style='raster',
|
style="raster",
|
||||||
autofocus_dz: int = 50,
|
autofocus_dz: int = 50,
|
||||||
use_video_port: bool = False,
|
use_video_port: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
resize: Tuple[int, int] = None,
|
||||||
bayer: bool = False,
|
bayer: bool = False,
|
||||||
fast_autofocus = False,
|
fast_autofocus=False,
|
||||||
metadata: dict = {},
|
metadata: dict = {},
|
||||||
tags: list = []):
|
tags: list = [],
|
||||||
|
):
|
||||||
|
|
||||||
# Generate a basename if none given
|
# Generate a basename if none given
|
||||||
if not basename:
|
if not basename:
|
||||||
|
|
@ -121,42 +123,47 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
initial_position = self.microscope.stage.position
|
initial_position = self.microscope.stage.position
|
||||||
|
|
||||||
# Add scan metadata
|
# Add scan metadata
|
||||||
if not 'time' in metadata:
|
if not "time" in metadata:
|
||||||
metadata['time'] = generate_basename()
|
metadata["time"] = generate_basename()
|
||||||
|
|
||||||
# Check if autofocus is enabled
|
# Check if autofocus is enabled
|
||||||
if autofocus_dz and hasattr(self.microscope.plugin, 'default_autofocus'):
|
if autofocus_dz and hasattr(self.microscope.plugin, "default_autofocus"):
|
||||||
autofocus_enabled = True
|
autofocus_enabled = True
|
||||||
else:
|
else:
|
||||||
autofocus_enabled = False
|
autofocus_enabled = False
|
||||||
|
|
||||||
if fast_autofocus and not hasattr(self.microscope.plugin.default_autofocus, 'monitor_sharpness'):
|
if fast_autofocus and not hasattr(
|
||||||
logging.error("Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?")
|
self.microscope.plugin.default_autofocus, "monitor_sharpness"
|
||||||
|
):
|
||||||
|
logging.error(
|
||||||
|
"Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?"
|
||||||
|
)
|
||||||
fast_autofocus = False
|
fast_autofocus = False
|
||||||
z_stack_dz = grid[2] * step_size[2] if grid[2] > 1 else 0 # shorthand for Z stack range
|
z_stack_dz = (
|
||||||
|
grid[2] * step_size[2] if grid[2] > 1 else 0
|
||||||
|
) # shorthand for Z stack range
|
||||||
|
|
||||||
# Construct an x-y grid (worry about z later)
|
# Construct an x-y grid (worry about z later)
|
||||||
x_y_grid = construct_grid(
|
x_y_grid = construct_grid(
|
||||||
initial_position,
|
initial_position, step_size[:2], grid[:2], style=style
|
||||||
step_size[:2],
|
|
||||||
grid[:2],
|
|
||||||
style=style
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep the initial Z position the same as our current position
|
# Keep the initial Z position the same as our current position
|
||||||
next_z = initial_position[2]
|
next_z = initial_position[2]
|
||||||
if fast_autofocus: # If fast autofocus is enabled, make
|
if fast_autofocus: # If fast autofocus is enabled, make
|
||||||
next_z += autofocus_dz/2 # sure we start from the top of the range
|
next_z += autofocus_dz / 2 # sure we start from the top of the range
|
||||||
initial_z = next_z # Save this value for use in raster scans
|
initial_z = next_z # Save this value for use in raster scans
|
||||||
|
|
||||||
# Now step through each point in the x-y coordinate array
|
# Now step through each point in the x-y coordinate array
|
||||||
for line in x_y_grid:
|
for line in x_y_grid:
|
||||||
# If rastering, rather than snake (or eventually spiral)
|
# If rastering, rather than snake (or eventually spiral)
|
||||||
# Return focus to initial position
|
# Return focus to initial position
|
||||||
if style == 'raster':
|
if style == "raster":
|
||||||
next_z = initial_z
|
next_z = initial_z
|
||||||
logging.debug("Returning to initial z position")
|
logging.debug("Returning to initial z position")
|
||||||
self.microscope.stage.move_abs([line[0][0], line[0][1], next_z]) #RWB: I think this line is redundant
|
self.microscope.stage.move_abs(
|
||||||
|
[line[0][0], line[0][1], next_z]
|
||||||
|
) # RWB: I think this line is redundant
|
||||||
|
|
||||||
for x_y in line:
|
for x_y in line:
|
||||||
# Move to new grid position without changing z
|
# Move to new grid position without changing z
|
||||||
|
|
@ -166,20 +173,21 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
if autofocus_enabled:
|
if autofocus_enabled:
|
||||||
if fast_autofocus:
|
if fast_autofocus:
|
||||||
self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus(
|
self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus(
|
||||||
dz=autofocus_dz,
|
dz=autofocus_dz,
|
||||||
target_z=-z_stack_dz/2.0, # Finish below the focus
|
target_z=-z_stack_dz / 2.0, # Finish below the focus
|
||||||
initial_move_up=False, # We're already at the top of the scan
|
initial_move_up=False, # We're already at the top of the scan
|
||||||
)
|
)
|
||||||
#TODO: save the focus data for future reference? Use it for diagnostics?
|
# TODO: save the focus data for future reference? Use it for diagnostics?
|
||||||
else:
|
else:
|
||||||
logging.debug("Running autofocus")
|
logging.debug("Running autofocus")
|
||||||
self.microscope.plugin.default_autofocus.autofocus(
|
self.microscope.plugin.default_autofocus.autofocus(
|
||||||
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz))
|
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
|
||||||
|
)
|
||||||
logging.debug("Finished autofocus")
|
logging.debug("Finished autofocus")
|
||||||
time.sleep(1) # TODO: Remove
|
time.sleep(1) # TODO: Remove
|
||||||
|
|
||||||
# If we're not doing a z-stack, just capture
|
# If we're not doing a z-stack, just capture
|
||||||
if (grid[2] <= 1):
|
if grid[2] <= 1:
|
||||||
self.capture(
|
self.capture(
|
||||||
basename,
|
basename,
|
||||||
scan_id,
|
scan_id,
|
||||||
|
|
@ -188,7 +196,7 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
resize=resize,
|
resize=resize,
|
||||||
bayer=bayer,
|
bayer=bayer,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
tags=tags
|
tags=tags,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug("Entering z-stack")
|
logging.debug("Entering z-stack")
|
||||||
|
|
@ -198,38 +206,43 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
scan_id=scan_id,
|
scan_id=scan_id,
|
||||||
step_size=step_size[2],
|
step_size=step_size[2],
|
||||||
steps=grid[2],
|
steps=grid[2],
|
||||||
center=not fast_autofocus, # fast_autofocus does this for us!
|
center=not fast_autofocus, # fast_autofocus does this for us!
|
||||||
return_to_start=not fast_autofocus,
|
return_to_start=not fast_autofocus,
|
||||||
use_video_port=use_video_port,
|
use_video_port=use_video_port,
|
||||||
resize=resize,
|
resize=resize,
|
||||||
bayer=bayer,
|
bayer=bayer,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
tags=tags
|
tags=tags,
|
||||||
)
|
)
|
||||||
# Make sure we use our current best estimate of focus (i.e. the current position) next point
|
# Make sure we use our current best estimate of focus (i.e. the current position) next point
|
||||||
next_z = self.microscope.stage.position[2]
|
next_z = self.microscope.stage.position[2]
|
||||||
if fast_autofocus:
|
if fast_autofocus:
|
||||||
next_z += autofocus_dz/2 # Fast autofocus requires us to start at the top of the range
|
next_z += (
|
||||||
|
autofocus_dz / 2
|
||||||
|
) # Fast autofocus requires us to start at the top of the range
|
||||||
if grid[2] > 1:
|
if grid[2] > 1:
|
||||||
next_z -= int(grid[2]/2.0*step_size[2]) # Z stacking means we're higher up to start with
|
next_z -= int(
|
||||||
|
grid[2] / 2.0 * step_size[2]
|
||||||
|
) # Z stacking means we're higher up to start with
|
||||||
|
|
||||||
logging.debug("Returning to {}".format(initial_position))
|
logging.debug("Returning to {}".format(initial_position))
|
||||||
self.microscope.stage.move_abs(initial_position)
|
self.microscope.stage.move_abs(initial_position)
|
||||||
|
|
||||||
def stack(
|
def stack(
|
||||||
self,
|
self,
|
||||||
basename: str = None,
|
basename: str = None,
|
||||||
temporary: bool = False,
|
temporary: bool = False,
|
||||||
scan_id: str = None,
|
scan_id: str = None,
|
||||||
step_size: int = 100,
|
step_size: int = 100,
|
||||||
steps: int = 5,
|
steps: int = 5,
|
||||||
center: bool = True,
|
center: bool = True,
|
||||||
return_to_start: bool = True,
|
return_to_start: bool = True,
|
||||||
use_video_port: bool = False,
|
use_video_port: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
resize: Tuple[int, int] = None,
|
||||||
bayer: bool = False,
|
bayer: bool = False,
|
||||||
metadata: dict = {},
|
metadata: dict = {},
|
||||||
tags: list = []):
|
tags: list = [],
|
||||||
|
):
|
||||||
|
|
||||||
# Generate a basename if none given
|
# Generate a basename if none given
|
||||||
if not basename:
|
if not basename:
|
||||||
|
|
@ -240,13 +253,12 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
scan_id = uuid.uuid4().hex
|
scan_id = uuid.uuid4().hex
|
||||||
|
|
||||||
# Add scan metadata
|
# Add scan metadata
|
||||||
if not 'time' in metadata:
|
if not "time" in metadata:
|
||||||
metadata['time'] = generate_basename()
|
metadata["time"] = generate_basename()
|
||||||
|
|
||||||
# Store initial position
|
# Store initial position
|
||||||
initial_position = self.microscope.stage.position
|
initial_position = self.microscope.stage.position
|
||||||
|
|
||||||
|
|
||||||
with self.microscope.lock:
|
with self.microscope.lock:
|
||||||
# Move to center scan
|
# Move to center scan
|
||||||
if center:
|
if center:
|
||||||
|
|
@ -264,7 +276,7 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
resize=resize,
|
resize=resize,
|
||||||
bayer=bayer,
|
bayer=bayer,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
tags=tags
|
tags=tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
if i != steps - 1:
|
if i != steps - 1:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI']
|
__all__ = ["Plugin", "HelloWorldAPI", "IdentifyAPI"]
|
||||||
|
|
||||||
from .plugin import Plugin
|
from .plugin import Plugin
|
||||||
from .api import HelloWorldAPI, IdentifyAPI
|
from .api import HelloWorldAPI, IdentifyAPI
|
||||||
|
|
@ -9,11 +9,14 @@ class IdentifyAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
A simple example API plugin, attached through the main microscope plugin.
|
A simple example API plugin, attached through the main microscope plugin.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Method to call when an HTTP GET request is made.
|
Method to call when an HTTP GET request is made.
|
||||||
"""
|
"""
|
||||||
data = self.plugin.identify() # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
|
data = (
|
||||||
|
self.plugin.identify()
|
||||||
|
) # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
|
||||||
return Response(escape(data))
|
return Response(escape(data))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -28,7 +31,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If the microscope does not already contain our plugin_string attribute
|
# If the microscope does not already contain our plugin_string attribute
|
||||||
if not hasattr(self.microscope, 'plugin_string'):
|
if not hasattr(self.microscope, "plugin_string"):
|
||||||
# Make a string, using the MicroscopeViewPlugin.plugin shortcut
|
# Make a string, using the MicroscopeViewPlugin.plugin shortcut
|
||||||
self.microscope.plugin_string = self.plugin.hello_world()
|
self.microscope.plugin_string = self.plugin.hello_world()
|
||||||
|
|
||||||
|
|
@ -43,7 +46,7 @@ class HelloWorldAPI(MicroscopeViewPlugin):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
# Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
|
# Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
|
||||||
new_plugin_string = payload.param('plugin_string', default='', convert=str)
|
new_plugin_string = payload.param("plugin_string", default="", convert=str)
|
||||||
|
|
||||||
if new_plugin_string: # If not None or empty
|
if new_plugin_string: # If not None or empty
|
||||||
# Set microscope attribute to the specified string
|
# Set microscope attribute to the specified string
|
||||||
|
|
@ -56,6 +59,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
An example API plugin that uses a long-running plugin method.
|
An example API plugin that uses a long-running plugin method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Method to call when an HTTP POST request is made.
|
Method to call when an HTTP POST request is made.
|
||||||
|
|
@ -64,7 +68,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
# Extract a values from the JSON payload.
|
# Extract a values from the JSON payload.
|
||||||
time_to_run = payload.param('time', default=10, convert=int)
|
time_to_run = payload.param("time", default=10, convert=int)
|
||||||
|
|
||||||
# Attach the long-running method as a microscope task
|
# Attach the long-running method as a microscope task
|
||||||
try:
|
try:
|
||||||
|
|
@ -79,6 +83,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
An example API plugin that uses a long-running but broken plugin method.
|
An example API plugin that uses a long-running but broken plugin method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Method to call when an HTTP POST request is made.
|
Method to call when an HTTP POST request is made.
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ class Plugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_views = {
|
api_views = {
|
||||||
'/identify': IdentifyAPI,
|
"/identify": IdentifyAPI,
|
||||||
'/hello': HelloWorldAPI,
|
"/hello": HelloWorldAPI,
|
||||||
'/long_running': LongRunningAPI,
|
"/long_running": LongRunningAPI,
|
||||||
'/some_exception': SomeExceptionAPI,
|
"/some_exception": SomeExceptionAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
def identify(self):
|
def identify(self):
|
||||||
|
|
@ -22,7 +22,9 @@ class Plugin(MicroscopePlugin):
|
||||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, self.microscope.stage)
|
response = "My parent camera is {}, and my parent stage is {}.".format(
|
||||||
|
self.microscope.camera, self.microscope.stage
|
||||||
|
)
|
||||||
print(response)
|
print(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -39,7 +41,7 @@ class Plugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
with self.microscope.lock:
|
with self.microscope.lock:
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
result = 15./0
|
result = 15.0 / 0
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import logging
|
||||||
|
|
||||||
|
|
||||||
class ConColors:
|
class ConColors:
|
||||||
HEADER = '\033[95m'
|
HEADER = "\033[95m"
|
||||||
OKBLUE = '\033[94m'
|
OKBLUE = "\033[94m"
|
||||||
OKGREEN = '\033[92m'
|
OKGREEN = "\033[92m"
|
||||||
WARNING = '\033[93m'
|
WARNING = "\033[93m"
|
||||||
FAIL = '\033[91m'
|
FAIL = "\033[91m"
|
||||||
ENDC = '\033[0m'
|
ENDC = "\033[0m"
|
||||||
BOLD = '\033[1m'
|
BOLD = "\033[1m"
|
||||||
UNDERLINE = '\033[4m'
|
UNDERLINE = "\033[4m"
|
||||||
|
|
||||||
|
|
||||||
def module_from_file(plugin_path):
|
def module_from_file(plugin_path):
|
||||||
|
|
@ -23,7 +23,11 @@ def module_from_file(plugin_path):
|
||||||
|
|
||||||
# Check if the path is to a file
|
# Check if the path is to a file
|
||||||
if not os.path.isfile(plugin_path):
|
if not os.path.isfile(plugin_path):
|
||||||
logging.warning(ConColors.FAIL + "No valid plugin found at {}.".format(plugin_path) + ConColors.ENDC)
|
logging.warning(
|
||||||
|
ConColors.FAIL
|
||||||
|
+ "No valid plugin found at {}.".format(plugin_path)
|
||||||
|
+ ConColors.ENDC
|
||||||
|
)
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
@ -35,6 +39,7 @@ def module_from_file(plugin_path):
|
||||||
|
|
||||||
return plugin_spec, plugin_module, plugin_name
|
return plugin_spec, plugin_module, plugin_name
|
||||||
|
|
||||||
|
|
||||||
def check_module(module_path):
|
def check_module(module_path):
|
||||||
"""
|
"""
|
||||||
Used to check if a module exists, without ever importing it.
|
Used to check if a module exists, without ever importing it.
|
||||||
|
|
@ -65,13 +70,13 @@ def check_module(module_path):
|
||||||
|
|
||||||
|
|
||||||
def name_from_module(plugin_path):
|
def name_from_module(plugin_path):
|
||||||
path_array = plugin_path.split('.')
|
path_array = plugin_path.split(".")
|
||||||
|
|
||||||
# Truncate namespace of default plugins
|
# Truncate namespace of default plugins
|
||||||
if path_array[0:2] == ['openflexure_microscope', 'plugins']:
|
if path_array[0:2] == ["openflexure_microscope", "plugins"]:
|
||||||
path_array = path_array[2:]
|
path_array = path_array[2:]
|
||||||
|
|
||||||
return '/'.join(path_array)
|
return "/".join(path_array)
|
||||||
|
|
||||||
|
|
||||||
def load_plugin_module(plugin_path):
|
def load_plugin_module(plugin_path):
|
||||||
|
|
@ -104,7 +109,13 @@ def load_plugin_class(plugin_path, plugin_class_name):
|
||||||
try:
|
try:
|
||||||
plugin_class = getattr(plugin_module, plugin_class_name)
|
plugin_class = getattr(plugin_module, plugin_class_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.warning(ConColors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + ConColors.ENDC)
|
logging.warning(
|
||||||
|
ConColors.FAIL
|
||||||
|
+ "Class {} does not exist in plugin {}. Skipping.".format(
|
||||||
|
plugin_class_name, plugin_path
|
||||||
|
)
|
||||||
|
+ ConColors.ENDC
|
||||||
|
)
|
||||||
return None, None
|
return None, None
|
||||||
else:
|
else:
|
||||||
return plugin_class, plugin_name
|
return plugin_class, plugin_name
|
||||||
|
|
@ -113,10 +124,14 @@ def load_plugin_class(plugin_path, plugin_class_name):
|
||||||
|
|
||||||
|
|
||||||
def class_from_map(plugin_map):
|
def class_from_map(plugin_map):
|
||||||
plugin_arr = plugin_map.split(':')
|
plugin_arr = plugin_map.split(":")
|
||||||
|
|
||||||
if not len(plugin_arr) == 2:
|
if not len(plugin_arr) == 2:
|
||||||
logging.warning(ConColors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + ConColors.ENDC)
|
logging.warning(
|
||||||
|
ConColors.WARNING
|
||||||
|
+ "Malformed plugin map {}. Skipping.".format(plugin_map)
|
||||||
|
+ ConColors.ENDC
|
||||||
|
)
|
||||||
return None, None
|
return None, None
|
||||||
else:
|
else:
|
||||||
return load_plugin_class(*plugin_arr)
|
return load_plugin_class(*plugin_arr)
|
||||||
|
|
@ -129,6 +144,7 @@ class PluginMount(object):
|
||||||
Args:
|
Args:
|
||||||
parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to.
|
parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.plugins = [] # List of plugin objects
|
self.plugins = [] # List of plugin objects
|
||||||
|
|
@ -141,13 +157,17 @@ class PluginMount(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def members(self):
|
def members(self):
|
||||||
ignores = ['state', 'members', 'attach']
|
ignores = ["state", "members", "attach"]
|
||||||
plugin_array = []
|
plugin_array = []
|
||||||
for obj_name in dir(self):
|
for obj_name in dir(self):
|
||||||
if not obj_name in ignores and not obj_name[:2] == '__':
|
if not obj_name in ignores and not obj_name[:2] == "__":
|
||||||
obj = getattr(self, obj_name)
|
obj = getattr(self, obj_name)
|
||||||
if isinstance(obj, MicroscopePlugin):
|
if isinstance(obj, MicroscopePlugin):
|
||||||
plugin_members = [member for member in inspect.getmembers(obj) if not member[0][:2] == '__']
|
plugin_members = [
|
||||||
|
member
|
||||||
|
for member in inspect.getmembers(obj)
|
||||||
|
if not member[0][:2] == "__"
|
||||||
|
]
|
||||||
plugin_info = (obj_name, plugin_members)
|
plugin_info = (obj_name, plugin_members)
|
||||||
plugin_array.append(plugin_info)
|
plugin_array.append(plugin_info)
|
||||||
return plugin_array
|
return plugin_array
|
||||||
|
|
@ -168,10 +188,20 @@ class PluginMount(object):
|
||||||
if plugin_class and plugin_name:
|
if plugin_class and plugin_name:
|
||||||
plugin_object = plugin_class()
|
plugin_object = plugin_class()
|
||||||
|
|
||||||
if hasattr(self, plugin_name): # If a plugin with the same name is already attached.
|
if hasattr(
|
||||||
logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC)
|
self, plugin_name
|
||||||
|
): # If a plugin with the same name is already attached.
|
||||||
|
logging.warning(
|
||||||
|
ConColors.WARNING
|
||||||
|
+ "A plugin named {} has already been loaded. Skipping {}.".format(
|
||||||
|
plugin_name, plugin_map
|
||||||
|
)
|
||||||
|
+ ConColors.ENDC
|
||||||
|
)
|
||||||
|
|
||||||
elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin
|
elif isinstance(
|
||||||
|
plugin_object, MicroscopePlugin
|
||||||
|
): # If plugin_object is an instance of MicroscopePlugin
|
||||||
# Attach plugin_object to the plugin mount
|
# Attach plugin_object to the plugin mount
|
||||||
setattr(self, pythonsafe_plugin_name, plugin_object)
|
setattr(self, pythonsafe_plugin_name, plugin_object)
|
||||||
self.plugins.append((plugin_name, plugin_object))
|
self.plugins.append((plugin_name, plugin_object))
|
||||||
|
|
@ -179,11 +209,16 @@ class PluginMount(object):
|
||||||
# Grant plugin access to the hardware
|
# Grant plugin access to the hardware
|
||||||
plugin_object.microscope = self.parent
|
plugin_object.microscope = self.parent
|
||||||
|
|
||||||
logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
|
logging.info(
|
||||||
|
ConColors.OKGREEN
|
||||||
|
+ "Plugin {} loaded as {}.".format(plugin_map, plugin_name)
|
||||||
|
+ ConColors.ENDC
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.error(f"Error loading plugin {plugin_map}. Moving on.")
|
logging.error(f"Error loading plugin {plugin_map}. Moving on.")
|
||||||
|
|
||||||
|
|
||||||
class MicroscopePlugin:
|
class MicroscopePlugin:
|
||||||
"""
|
"""
|
||||||
Parent class for all microscope plugins.
|
Parent class for all microscope plugins.
|
||||||
|
|
@ -195,4 +230,6 @@ class MicroscopePlugin:
|
||||||
api_views = {} # Initially empty dictionary of API views associated with the plugin
|
api_views = {} # Initially empty dictionary of API views associated with the plugin
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.microscope = None #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object
|
self.microscope = (
|
||||||
|
None
|
||||||
|
) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
from openflexure_microscope.devel import MicroscopeViewPlugin, TaskDeniedException, JsonResponse, Response, request, escape, jsonify
|
from openflexure_microscope.devel import (
|
||||||
|
MicroscopeViewPlugin,
|
||||||
|
TaskDeniedException,
|
||||||
|
JsonResponse,
|
||||||
|
Response,
|
||||||
|
request,
|
||||||
|
escape,
|
||||||
|
jsonify,
|
||||||
|
)
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class DoAPI(MicroscopeViewPlugin):
|
class DoAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
A simple example API plugin
|
A simple example API plugin
|
||||||
|
|
@ -15,27 +24,21 @@ class DoAPI(MicroscopeViewPlugin):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
# Extract a values from the JSON payload.
|
# Extract a values from the JSON payload.
|
||||||
val_int = payload.param('val_int', default=None, convert=int)
|
val_int = payload.param("val_int", default=None, convert=int)
|
||||||
val_str = payload.param('val_str', default=None, convert=str)
|
val_str = payload.param("val_str", default=None, convert=str)
|
||||||
val_radio = payload.param('val_radio', default=None)
|
val_radio = payload.param("val_radio", default=None)
|
||||||
val_check = payload.param('val_check', default=None)
|
val_check = payload.param("val_check", default=None)
|
||||||
val_select = payload.param('val_select', default=None)
|
val_select = payload.param("val_select", default=None)
|
||||||
val_disposable = payload.param('val_disposable', default=None)
|
val_disposable = payload.param("val_disposable", default=None)
|
||||||
|
|
||||||
self.plugin.set_values(
|
self.plugin.set_values(
|
||||||
val_int,
|
val_int, val_str, val_radio, val_check, val_select, val_disposable
|
||||||
val_str,
|
|
||||||
val_radio,
|
|
||||||
val_check,
|
|
||||||
val_select,
|
|
||||||
val_disposable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print(self.plugin.get_values_dict())
|
print(self.plugin.get_values_dict())
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({"response": "completed"})
|
||||||
'response': 'completed'
|
|
||||||
})
|
|
||||||
|
|
||||||
class TaskAPI(MicroscopeViewPlugin):
|
class TaskAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
@ -43,19 +46,19 @@ class TaskAPI(MicroscopeViewPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return jsonify({
|
return jsonify({"run_time": self.plugin.run_time})
|
||||||
'run_time': self.plugin.run_time
|
|
||||||
})
|
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
# Get payload JSON
|
# Get payload JSON
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
# Extract a values from the JSON payload.
|
# Extract a values from the JSON payload.
|
||||||
val_int = payload.param('run_time', default=5, convert=int)
|
val_int = payload.param("run_time", default=5, convert=int)
|
||||||
|
|
||||||
logging.info("Running task...")
|
logging.info("Running task...")
|
||||||
task = self.microscope.task.start(self.plugin.generate_random_numbers_for_a_while, val_int)
|
task = self.microscope.task.start(
|
||||||
|
self.plugin.generate_random_numbers_for_a_while, val_int
|
||||||
|
)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 202
|
||||||
|
|
@ -9,19 +9,18 @@ from .api import DoAPI, TaskAPI
|
||||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||||
FORM_PATH = os.path.join(HERE, "forms.json")
|
FORM_PATH = os.path.join(HERE, "forms.json")
|
||||||
|
|
||||||
|
|
||||||
class ExamplePlugin(MicroscopePlugin):
|
class ExamplePlugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
An example plugin using a comprehensive form
|
An example plugin using a comprehensive form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global FORM_PATH
|
global FORM_PATH
|
||||||
|
|
||||||
with open(FORM_PATH, 'r') as sc:
|
with open(FORM_PATH, "r") as sc:
|
||||||
api_form = json.load(sc)
|
api_form = json.load(sc)
|
||||||
|
|
||||||
api_views = {
|
api_views = {"/do": DoAPI, "/task": TaskAPI}
|
||||||
'/do': DoAPI,
|
|
||||||
'/task': TaskAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.val_int = 10
|
self.val_int = 10
|
||||||
|
|
@ -33,7 +32,9 @@ class ExamplePlugin(MicroscopePlugin):
|
||||||
|
|
||||||
self.run_time = 5
|
self.run_time = 5
|
||||||
|
|
||||||
def set_values(self, val_int, val_str, val_radio, val_check, val_select, val_disposable):
|
def set_values(
|
||||||
|
self, val_int, val_str, val_radio, val_check, val_select, val_disposable
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Demonstrate a plugin with form
|
Demonstrate a plugin with form
|
||||||
"""
|
"""
|
||||||
|
|
@ -54,12 +55,12 @@ class ExamplePlugin(MicroscopePlugin):
|
||||||
|
|
||||||
def get_values_dict(self):
|
def get_values_dict(self):
|
||||||
return {
|
return {
|
||||||
'val_int': self.val_int,
|
"val_int": self.val_int,
|
||||||
'val_str': self.val_str,
|
"val_str": self.val_str,
|
||||||
'val_radio': self.val_radio,
|
"val_radio": self.val_radio,
|
||||||
'val_check': self.val_check,
|
"val_check": self.val_check,
|
||||||
'val_select': self.val_select,
|
"val_select": self.val_select,
|
||||||
'val_unused': self.val_unused
|
"val_unused": self.val_unused,
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_random_numbers_for_a_while(self, run_time: int):
|
def generate_random_numbers_for_a_while(self, run_time: int):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
__all__ = ['base', 'mock', 'sanga']
|
__all__ = ["base", "mock", "sanga"]
|
||||||
|
|
||||||
from . import 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
|
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
|
||||||
access to camera hardware
|
access to camera hardware
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.lock = StrictLock(timeout=5)
|
self.lock = StrictLock(timeout=5)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from openflexure_microscope.utilities import axes_to_array
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class MockStage(BaseStage):
|
class MockStage(BaseStage):
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port=None, **kwargs):
|
||||||
BaseStage.__init__(self)
|
BaseStage.__init__(self)
|
||||||
|
|
@ -15,13 +16,13 @@ class MockStage(BaseStage):
|
||||||
def state(self):
|
def state(self):
|
||||||
"""The general state dictionary of the board."""
|
"""The general state dictionary of the board."""
|
||||||
state = {
|
state = {
|
||||||
'position': {
|
"position": {
|
||||||
'x': self.position[0],
|
"x": self.position[0],
|
||||||
'y': self.position[1],
|
"y": self.position[1],
|
||||||
'z': self.position[2],
|
"z": self.position[2],
|
||||||
},
|
},
|
||||||
'board': None,
|
"board": None,
|
||||||
'version': '0'
|
"version": "0",
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
@ -29,21 +30,15 @@ class MockStage(BaseStage):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
|
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
if 'backlash' in config:
|
if "backlash" in config:
|
||||||
# Construct backlash array
|
# Construct backlash array
|
||||||
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
|
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
||||||
self.backlash = backlash
|
self.backlash = backlash
|
||||||
|
|
||||||
def read_config(self) -> dict:
|
def read_config(self) -> dict:
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
blsh = self.backlash.tolist()
|
blsh = self.backlash.tolist()
|
||||||
config = {
|
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
||||||
'backlash': {
|
|
||||||
'x': blsh[0],
|
|
||||||
'y': blsh[1],
|
|
||||||
'z': blsh[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -69,7 +64,7 @@ class MockStage(BaseStage):
|
||||||
assert len(blsh) == self.n_axes
|
assert len(blsh) == self.n_axes
|
||||||
self._backlash = np.array(blsh)
|
self._backlash = np.array(blsh)
|
||||||
else:
|
else:
|
||||||
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
|
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||||
|
|
||||||
def move_rel(self, displacement, axis=None, backlash=True):
|
def move_rel(self, displacement, axis=None, backlash=True):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,29 @@ class SangaStage(BaseStage):
|
||||||
board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object.
|
board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object.
|
||||||
_backlash (list): 3-element (element-per-axis) list of backlash compensation in steps.
|
_backlash (list): 3-element (element-per-axis) list of backlash compensation in steps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port=None, **kwargs):
|
||||||
"""Class managing serial communications with the motors for an Openflexure stage"""
|
"""Class managing serial communications with the motors for an Openflexure stage"""
|
||||||
BaseStage.__init__(self)
|
BaseStage.__init__(self)
|
||||||
|
|
||||||
self.board = Sangaboard(port, **kwargs)
|
self.board = Sangaboard(port, **kwargs)
|
||||||
|
|
||||||
self._backlash = None # Initialise backlash storage, used by property setter/getter
|
self._backlash = (
|
||||||
self.axis_names = ['x', 'y', 'z'] # Assume all sangaboards are 3 axis
|
None
|
||||||
|
) # Initialise backlash storage, used by property setter/getter
|
||||||
|
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""The general state dictionary of the board."""
|
"""The general state dictionary of the board."""
|
||||||
state = {
|
state = {
|
||||||
'position': {
|
"position": {
|
||||||
'x': self.position[0],
|
"x": self.position[0],
|
||||||
'y': self.position[1],
|
"y": self.position[1],
|
||||||
'z': self.position[2],
|
"z": self.position[2],
|
||||||
},
|
},
|
||||||
'board': self.board.board,
|
"board": self.board.board,
|
||||||
'firmware': self.board.firmware
|
"firmware": self.board.firmware,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
@ -80,27 +83,21 @@ class SangaStage(BaseStage):
|
||||||
assert len(blsh) == self.n_axes
|
assert len(blsh) == self.n_axes
|
||||||
self._backlash = np.array(blsh)
|
self._backlash = np.array(blsh)
|
||||||
else:
|
else:
|
||||||
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
|
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_config(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
|
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
if 'backlash' in config:
|
if "backlash" in config:
|
||||||
# Construct backlash array
|
# Construct backlash array
|
||||||
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
|
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
||||||
self.backlash = backlash
|
self.backlash = backlash
|
||||||
|
|
||||||
def read_config(self) -> dict:
|
def read_config(self) -> dict:
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
blsh = self.backlash.tolist()
|
blsh = self.backlash.tolist()
|
||||||
config = {
|
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
||||||
'backlash': {
|
|
||||||
'x': blsh[0],
|
|
||||||
'y': blsh[1],
|
|
||||||
'z': blsh[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
@ -117,7 +114,9 @@ class SangaStage(BaseStage):
|
||||||
if axis is not None:
|
if axis is not None:
|
||||||
# backlash correction is easier if we're always in 3D
|
# backlash correction is easier if we're always in 3D
|
||||||
# so this code just converts single-axis moves into all-axis moves.
|
# so this code just converts single-axis moves into all-axis moves.
|
||||||
assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names)
|
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||||
|
self.axis_names
|
||||||
|
)
|
||||||
move = np.zeros(self.n_axes, dtype=np.int)
|
move = np.zeros(self.n_axes, dtype=np.int)
|
||||||
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
||||||
displacement = move
|
displacement = move
|
||||||
|
|
@ -134,7 +133,7 @@ class SangaStage(BaseStage):
|
||||||
initial_move -= np.where(
|
initial_move -= np.where(
|
||||||
self.backlash * displacement < 0,
|
self.backlash * displacement < 0,
|
||||||
self.backlash,
|
self.backlash,
|
||||||
np.zeros(self.n_axes, dtype=self.backlash.dtype)
|
np.zeros(self.n_axes, dtype=self.backlash.dtype),
|
||||||
)
|
)
|
||||||
self.board.move_rel(initial_move)
|
self.board.move_rel(initial_move)
|
||||||
if np.any(displacement - initial_move != 0):
|
if np.any(displacement - initial_move != 0):
|
||||||
|
|
@ -160,7 +159,9 @@ class SangaStage(BaseStage):
|
||||||
"""
|
"""
|
||||||
starting_position = self.position
|
starting_position = self.position
|
||||||
rel_positions = np.array(rel_positions)
|
rel_positions = np.array(rel_positions)
|
||||||
assert rel_positions.shape[1] == 3, ValueError("Positions should be 3 elements long.")
|
assert rel_positions.shape[1] == 3, ValueError(
|
||||||
|
"Positions should be 3 elements long."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.move_rel(rel_positions[0], backlash=backlash)
|
self.move_rel(rel_positions[0], backlash=backlash)
|
||||||
yield 0
|
yield 0
|
||||||
|
|
@ -186,7 +187,7 @@ class SangaStage(BaseStage):
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Cleanly close communication with the stage"""
|
"""Cleanly close communication with the stage"""
|
||||||
if hasattr(self, 'board'):
|
if hasattr(self, "board"):
|
||||||
self.board.close()
|
self.board.close()
|
||||||
|
|
||||||
# Methods specific to Sangaboard
|
# Methods specific to Sangaboard
|
||||||
|
|
@ -205,12 +206,16 @@ class SangaStage(BaseStage):
|
||||||
need to worry about that here.
|
need to worry about that here.
|
||||||
"""
|
"""
|
||||||
if type is not None:
|
if type is not None:
|
||||||
print("An exception occurred inside a with block, resetting position \
|
print(
|
||||||
to its value at the start of the with block")
|
"An exception occurred inside a with block, resetting position \
|
||||||
|
to its value at the start of the with block"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self.move_abs(self._position_on_enter)
|
self.move_abs(self._position_on_enter)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("A further exception occurred when resetting position: {}".format(e))
|
print(
|
||||||
|
"A further exception occurred when resetting position: {}".format(e)
|
||||||
|
)
|
||||||
print("Move completed, raising exception...")
|
print("Move completed, raising exception...")
|
||||||
raise value # Propagate the exception
|
raise value # Propagate the exception
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import time
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class ExtensibleSerialInstrument(object):
|
class ExtensibleSerialInstrument(object):
|
||||||
"""
|
"""
|
||||||
An instrument that communicates by sending strings back and forth over serial
|
An instrument that communicates by sending strings back and forth over serial
|
||||||
|
|
@ -49,8 +50,13 @@ class ExtensibleSerialInstrument(object):
|
||||||
blocked for a long time. The lock is reentrant so there's no issue with
|
blocked for a long time. The lock is reentrant so there's no issue with
|
||||||
acquiring it twice.
|
acquiring it twice.
|
||||||
"""
|
"""
|
||||||
termination_character = "\n" #: All messages to or from the instrument end with this character.
|
|
||||||
termination_line = None #: If multi-line responses are recieved, they must end with this string
|
termination_character = (
|
||||||
|
"\n"
|
||||||
|
) #: All messages to or from the instrument end with this character.
|
||||||
|
termination_line = (
|
||||||
|
None
|
||||||
|
) #: If multi-line responses are recieved, they must end with this string
|
||||||
ignore_echo = False
|
ignore_echo = False
|
||||||
port_settings = {}
|
port_settings = {}
|
||||||
|
|
||||||
|
|
@ -61,7 +67,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
logging.info("Updating ESI port settings")
|
logging.info("Updating ESI port settings")
|
||||||
self.port_settings.update(kwargs)
|
self.port_settings.update(kwargs)
|
||||||
logging.info("Opening ESI connection to port {}".format(port))
|
logging.info("Opening ESI connection to port {}".format(port))
|
||||||
self.open(port, False) # Eventually this shouldn't rely on init...
|
self.open(port, False) # Eventually this shouldn't rely on init...
|
||||||
logging.info("Opened ESI connection to port {}".format(port))
|
logging.info("Opened ESI connection to port {}".format(port))
|
||||||
|
|
||||||
def open(self, port=None, quiet=True):
|
def open(self, port=None, quiet=True):
|
||||||
|
|
@ -71,17 +77,22 @@ class ExtensibleSerialInstrument(object):
|
||||||
then we don't warn when ports are opened multiple times.
|
then we don't warn when ports are opened multiple times.
|
||||||
"""
|
"""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
if hasattr(self,'_ser') and self._ser.isOpen():
|
if hasattr(self, "_ser") and self._ser.isOpen():
|
||||||
if not quiet: logging.warning("Attempted to open an already-open port!")
|
if not quiet:
|
||||||
|
logging.warning("Attempted to open an already-open port!")
|
||||||
return
|
return
|
||||||
if port is None:
|
if port is None:
|
||||||
port=self.find_port()
|
port = self.find_port()
|
||||||
assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
|
assert (
|
||||||
self._ser = serial.Serial(port,**self.port_settings)
|
port is not None
|
||||||
#the block above wraps the serial IO layer with a text IO layer
|
), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
|
||||||
#this allows us to read/write in neat lines. NB the buffer size must
|
self._ser = serial.Serial(port, **self.port_settings)
|
||||||
#be set to 1 byte for maximum responsiveness.
|
# the block above wraps the serial IO layer with a text IO layer
|
||||||
assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?"
|
# this allows us to read/write in neat lines. NB the buffer size must
|
||||||
|
# be set to 1 byte for maximum responsiveness.
|
||||||
|
assert (
|
||||||
|
self.test_communications()
|
||||||
|
), "The instrument doesn't seem to be responding. Did you specify the right port?"
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Cleanly close the device. Includes proper logging statements."""
|
"""Cleanly close the device. Includes proper logging statements."""
|
||||||
|
|
@ -95,11 +106,12 @@ class ExtensibleSerialInstrument(object):
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Emergency close the device. Try to avoid having to use this."""
|
"""Emergency close the device. Try to avoid having to use this."""
|
||||||
if hasattr(self, '_ser') and self._ser.isOpen():
|
if hasattr(self, "_ser") and self._ser.isOpen():
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
print(
|
print(
|
||||||
"Closing an open serial communication has been triggered by garbage collection!\n"\
|
"Closing an open serial communication has been triggered by garbage collection!\n"
|
||||||
"Please close this device more sensibly in future.")
|
"Please close this device more sensibly in future."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self._ser.close()
|
self._ser.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -112,29 +124,38 @@ class ExtensibleSerialInstrument(object):
|
||||||
"""Cleanly close down the instrument at end of a with block."""
|
"""Cleanly close down the instrument at end of a with block."""
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def write(self,query_string):
|
def write(self, query_string):
|
||||||
"""Write a string to the serial port"""
|
"""Write a string to the serial port"""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
assert self._ser.isOpen(), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
|
assert (
|
||||||
#TODO: Check if this code is needed and if not kill it
|
self._ser.isOpen()
|
||||||
# try:
|
), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
|
||||||
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
|
# TODO: Check if this code is needed and if not kill it
|
||||||
# except AttributeError:
|
# try:
|
||||||
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting
|
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
|
||||||
data=query_string+self.termination_character
|
# except AttributeError:
|
||||||
data=data.encode()
|
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting
|
||||||
|
data = query_string + self.termination_character
|
||||||
|
data = data.encode()
|
||||||
self._ser.write(data)
|
self._ser.write(data)
|
||||||
|
|
||||||
def flush_input_buffer(self):
|
def flush_input_buffer(self):
|
||||||
"""Make sure there's nothing waiting to be read, and clear the buffer if there is."""
|
"""Make sure there's nothing waiting to be read, and clear the buffer if there is."""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
if self._ser.inWaiting()>0: self._ser.flushInput()
|
if self._ser.inWaiting() > 0:
|
||||||
|
self._ser.flushInput()
|
||||||
|
|
||||||
def readline(self, timeout=None):
|
def readline(self, timeout=None):
|
||||||
"""Read one line from the serial port."""
|
"""Read one line from the serial port."""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
return self._ser.readline().decode('utf8').replace(self.termination_character,"\n")
|
return (
|
||||||
|
self._ser.readline()
|
||||||
|
.decode("utf8")
|
||||||
|
.replace(self.termination_character, "\n")
|
||||||
|
)
|
||||||
|
|
||||||
_communications_lock = None
|
_communications_lock = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def communications_lock(self):
|
def communications_lock(self):
|
||||||
"""A lock object used to protect access to the communications bus"""
|
"""A lock object used to protect access to the communications bus"""
|
||||||
|
|
@ -152,15 +173,19 @@ class ExtensibleSerialInstrument(object):
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
if termination_line is None:
|
if termination_line is None:
|
||||||
termination_line = self.termination_line
|
termination_line = self.termination_line
|
||||||
assert isinstance(termination_line, str), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument."
|
assert isinstance(
|
||||||
|
termination_line, str
|
||||||
|
), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument."
|
||||||
response = ""
|
response = ""
|
||||||
last_line = "dummy"
|
last_line = "dummy"
|
||||||
while termination_line not in last_line and len(last_line) > 0: #read until we get the termination line.
|
while (
|
||||||
|
termination_line not in last_line and len(last_line) > 0
|
||||||
|
): # read until we get the termination line.
|
||||||
last_line = self.readline(timeout)
|
last_line = self.readline(timeout)
|
||||||
response += last_line
|
response += last_line
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def query(self,queryString,multiline=False,termination_line=None,timeout=None):
|
def query(self, queryString, multiline=False, termination_line=None, timeout=None):
|
||||||
"""
|
"""
|
||||||
Write a string to the stage controller and return its response.
|
Write a string to the stage controller and return its response.
|
||||||
|
|
||||||
|
|
@ -170,12 +195,12 @@ class ExtensibleSerialInstrument(object):
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
self.flush_input_buffer()
|
self.flush_input_buffer()
|
||||||
self.write(queryString)
|
self.write(queryString)
|
||||||
if self.ignore_echo == True: # Needs Implementing for a multiline read!
|
if self.ignore_echo == True: # Needs Implementing for a multiline read!
|
||||||
first_line = self.readline(timeout).strip()
|
first_line = self.readline(timeout).strip()
|
||||||
if first_line == queryString:
|
if first_line == queryString:
|
||||||
return self.readline(timeout).strip()
|
return self.readline(timeout).strip()
|
||||||
else:
|
else:
|
||||||
logging.info('This command did not echo!!!')
|
logging.info("This command did not echo!!!")
|
||||||
return first_line
|
return first_line
|
||||||
|
|
||||||
if termination_line is not None:
|
if termination_line is not None:
|
||||||
|
|
@ -183,9 +208,18 @@ class ExtensibleSerialInstrument(object):
|
||||||
if multiline:
|
if multiline:
|
||||||
return self.read_multiline(termination_line)
|
return self.read_multiline(termination_line)
|
||||||
else:
|
else:
|
||||||
return self.readline(timeout).strip() #question: should we strip the final newline?
|
return self.readline(
|
||||||
|
timeout
|
||||||
|
).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.
|
Perform a query, returning a parsed form of the response.
|
||||||
|
|
||||||
|
|
@ -204,46 +238,81 @@ class ExtensibleSerialInstrument(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response_regex = response_string
|
response_regex = response_string
|
||||||
noop = lambda x: x #placeholder null parse function
|
noop = lambda x: x # placeholder null parse function
|
||||||
placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function)
|
placeholders = [ # tuples of (regex matching placeholder, regex to replace it with, parse function)
|
||||||
(r"%c", r".", noop),
|
(r"%c", r".", noop),
|
||||||
(r"%(\d+)c", r".{\1}", noop), #TODO support %cn where n is a number of chars
|
(
|
||||||
|
r"%(\d+)c",
|
||||||
|
r".{\1}",
|
||||||
|
noop,
|
||||||
|
), # TODO support %cn where n is a number of chars
|
||||||
(r"%d", r"[-+]?\\d+", int),
|
(r"%d", r"[-+]?\\d+", int),
|
||||||
(r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float),
|
(r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float),
|
||||||
(r"%i", r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", lambda x: int(x, 0)), #0=autodetect base
|
(
|
||||||
(r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal
|
r"%i",
|
||||||
|
r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)",
|
||||||
|
lambda x: int(x, 0),
|
||||||
|
), # 0=autodetect base
|
||||||
|
(r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), # 8 means octal
|
||||||
(r"%s", r"\\s+", noop),
|
(r"%s", r"\\s+", noop),
|
||||||
(r"%u", r"\\d+", int),
|
(r"%u", r"\\d+", int),
|
||||||
(r"%[xX]", r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", lambda x: int(x, 16)), #16 forces hexadecimal
|
(
|
||||||
|
r"%[xX]",
|
||||||
|
r"[-+]?(?:0[xX])?[\\dA-Fa-f]+",
|
||||||
|
lambda x: int(x, 16),
|
||||||
|
), # 16 forces hexadecimal
|
||||||
]
|
]
|
||||||
matched_placeholders = []
|
matched_placeholders = []
|
||||||
for placeholder, regex, parse_fun in placeholders:
|
for placeholder, regex, parse_fun in placeholders:
|
||||||
response_regex = re.sub(placeholder, '('+regex+')', response_regex) #substitute regex for placeholder
|
response_regex = re.sub(
|
||||||
matched_placeholders.extend([(parse_fun, m.start()) for m in re.finditer(placeholder, response_string)]) #save the positions of the placeholders
|
placeholder, "(" + regex + ")", response_regex
|
||||||
|
) # substitute regex for placeholder
|
||||||
|
matched_placeholders.extend(
|
||||||
|
[
|
||||||
|
(parse_fun, m.start())
|
||||||
|
for m in re.finditer(placeholder, response_string)
|
||||||
|
]
|
||||||
|
) # save the positions of the placeholders
|
||||||
if parse_function is None:
|
if parse_function is None:
|
||||||
parse_function = [f for f, s in sorted(matched_placeholders, key=lambda m: m[1])] #order parse functions by their occurrence in the original string
|
parse_function = [
|
||||||
if not hasattr(parse_function,'__iter__'):
|
f for f, s in sorted(matched_placeholders, key=lambda m: m[1])
|
||||||
parse_function = [parse_function] #make sure it's a list.
|
] # order parse functions by their occurrence in the original string
|
||||||
|
if not hasattr(parse_function, "__iter__"):
|
||||||
|
parse_function = [parse_function] # make sure it's a list.
|
||||||
|
|
||||||
reply = self.query(query_string, **kwargs) #do the query
|
reply = self.query(query_string, **kwargs) # do the query
|
||||||
#if match this could be because another response entered the buffer between write and read. Sleep for short while then
|
# if match this could be because another response entered the buffer between write and read. Sleep for short while then
|
||||||
#check if something is now in the buffer, while the buffer is not empty repeat regex
|
# check if something is now in the buffer, while the buffer is not empty repeat regex
|
||||||
waited=False
|
waited = False
|
||||||
res=re.search(response_regex, reply, flags=re_flags)
|
res = re.search(response_regex, reply, flags=re_flags)
|
||||||
while res is None:
|
while res is None:
|
||||||
if not waited:
|
if not waited:
|
||||||
time.sleep(.1)
|
time.sleep(0.1)
|
||||||
waited=True
|
waited = True
|
||||||
original_reply = reply
|
original_reply = reply
|
||||||
if self._ser.inWaiting():
|
if self._ser.inWaiting():
|
||||||
reply = self.readline().strip()
|
reply = self.readline().strip()
|
||||||
res=re.search(response_regex, reply, flags=re_flags)
|
res = re.search(response_regex, reply, flags=re_flags)
|
||||||
if res is not None:
|
if res is not None:
|
||||||
warnings.warn("Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"%(original_reply, query_string, response_string, response_regex), RuntimeWarning)
|
warnings.warn(
|
||||||
|
"Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"
|
||||||
|
% (
|
||||||
|
original_reply,
|
||||||
|
query_string,
|
||||||
|
response_string,
|
||||||
|
response_regex,
|
||||||
|
),
|
||||||
|
RuntimeWarning,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)" % (query_string, original_reply, response_string, response_regex))
|
raise ValueError(
|
||||||
|
"Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)"
|
||||||
|
% (query_string, original_reply, response_string, response_regex)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
parsed_result= [f(g) for f, g in zip(parse_function, res.groups())] #try to apply each parse function to its argument
|
parsed_result = [
|
||||||
|
f(g) for f, g in zip(parse_function, res.groups())
|
||||||
|
] # try to apply each parse function to its argument
|
||||||
if len(parsed_result) == 1:
|
if len(parsed_result) == 1:
|
||||||
return parsed_result[0]
|
return parsed_result[0]
|
||||||
else:
|
else:
|
||||||
|
|
@ -251,10 +320,15 @@ class ExtensibleSerialInstrument(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.info("Matched Groups: {}".format(res.groups()))
|
logging.info("Matched Groups: {}".format(res.groups()))
|
||||||
logging.info("Parsing Functions {}:".format(parse_function))
|
logging.info("Parsing Functions {}:".format(parse_function))
|
||||||
raise ValueError("Stage response to %s ('%s') couldn't be parsed by the supplied function" % (query_string, reply))
|
raise ValueError(
|
||||||
|
"Stage response to %s ('%s') couldn't be parsed by the supplied function"
|
||||||
|
% (query_string, reply)
|
||||||
|
)
|
||||||
|
|
||||||
def int_query(self, query_string, **kwargs):
|
def int_query(self, query_string, **kwargs):
|
||||||
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
|
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
|
||||||
return self.parsed_query(query_string, "%d", **kwargs)
|
return self.parsed_query(query_string, "%d", **kwargs)
|
||||||
|
|
||||||
def float_query(self, query_string, **kwargs):
|
def float_query(self, query_string, **kwargs):
|
||||||
"""Perform a query and return the result(s) as float(s) (see parsedQuery)"""
|
"""Perform a query and return the result(s) as float(s) (see parsedQuery)"""
|
||||||
return self.parsed_query(query_string, "%f", **kwargs)
|
return self.parsed_query(query_string, "%f", **kwargs)
|
||||||
|
|
@ -273,7 +347,13 @@ class ExtensibleSerialInstrument(object):
|
||||||
if our instrument is there."""
|
if our instrument is there."""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
success = False
|
success = False
|
||||||
for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?!
|
for (
|
||||||
|
port_name,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) in (
|
||||||
|
serial.tools.list_ports.comports()
|
||||||
|
): # loop through serial ports, apparently 256 is the limit?!
|
||||||
try:
|
try:
|
||||||
logging.info("Trying port {}".format(port_name))
|
logging.info("Trying port {}".format(port_name))
|
||||||
self.open(port_name)
|
self.open(port_name)
|
||||||
|
|
@ -285,14 +365,15 @@ class ExtensibleSerialInstrument(object):
|
||||||
try:
|
try:
|
||||||
self.close()
|
self.close()
|
||||||
except:
|
except:
|
||||||
pass #we don't care if there's an error closing the port...
|
pass # we don't care if there's an error closing the port...
|
||||||
if success:
|
if success:
|
||||||
break #again, make sure this happens *after* closing the port
|
break # again, make sure this happens *after* closing the port
|
||||||
if success:
|
if success:
|
||||||
return port_name
|
return port_name
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class OptionalModule(object):
|
class OptionalModule(object):
|
||||||
"""This allows a `ExtensibleSerialInstrument` to have optional features.
|
"""This allows a `ExtensibleSerialInstrument` to have optional features.
|
||||||
|
|
||||||
|
|
@ -301,17 +382,27 @@ class OptionalModule(object):
|
||||||
the serial instrument, and can be added or removed at run-time.
|
the serial instrument, and can be added or removed at run-time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,available,parent=None,module_type="Undefined",model="Generic"):
|
def __init__(
|
||||||
assert type(available) is bool, 'Option module availablity should be a boolean not a {}'.format(type(available))
|
self, available, parent=None, module_type="Undefined", model="Generic"
|
||||||
self._available=available
|
):
|
||||||
self._parent=parent
|
assert (
|
||||||
assert type(module_type) is str, 'Option module type should be a string not a {}'.format(type(module_typ))
|
type(available) is bool
|
||||||
self.module_type=module_type
|
), "Option module availablity should be a boolean not a {}".format(
|
||||||
|
type(available)
|
||||||
|
)
|
||||||
|
self._available = available
|
||||||
|
self._parent = parent
|
||||||
|
assert (
|
||||||
|
type(module_type) is str
|
||||||
|
), "Option module type should be a string not a {}".format(type(module_typ))
|
||||||
|
self.module_type = module_type
|
||||||
if available:
|
if available:
|
||||||
assert type(model) is str, 'Option module type should be a string not a {}'.format(type(model))
|
assert (
|
||||||
self.model=model
|
type(model) is str
|
||||||
|
), "Option module type should be a string not a {}".format(type(model))
|
||||||
|
self.model = model
|
||||||
else:
|
else:
|
||||||
self.model=None
|
self.model = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
|
|
@ -319,11 +410,11 @@ class OptionalModule(object):
|
||||||
|
|
||||||
def confirm_available(self):
|
def confirm_available(self):
|
||||||
"""Check if module is available, no return, will raise exception if not available!"""
|
"""Check if module is available, no return, will raise exception if not available!"""
|
||||||
assert self._available, "No \"{}\" supported on firmware".format(self.module_type)
|
assert self._available, 'No "{}" supported on firmware'.format(self.module_type)
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
"""Consistently spaced desciption for listing modules"""
|
"""Consistently spaced desciption for listing modules"""
|
||||||
return self.module_type+" "*(25-len(self.module_type))+"- "+self.model
|
return self.module_type + " " * (25 - len(self.module_type)) + "- " + self.model
|
||||||
|
|
||||||
|
|
||||||
class QueriedProperty(object):
|
class QueriedProperty(object):
|
||||||
|
|
@ -359,8 +450,18 @@ class QueriedProperty(object):
|
||||||
:ack_writes:
|
:ack_writes:
|
||||||
set to "readline" to discard a line of input after writing.
|
set to "readline" to discard a line of input after writing.
|
||||||
"""
|
"""
|
||||||
def __init__(self, get_cmd=None, set_cmd=None, validate=None, valrange=None,
|
|
||||||
fdel=None, doc=None, response_string=None, ack_writes="no"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
get_cmd=None,
|
||||||
|
set_cmd=None,
|
||||||
|
validate=None,
|
||||||
|
valrange=None,
|
||||||
|
fdel=None,
|
||||||
|
doc=None,
|
||||||
|
response_string=None,
|
||||||
|
ack_writes="no",
|
||||||
|
):
|
||||||
self.response_string = response_string
|
self.response_string = response_string
|
||||||
self.get_cmd = get_cmd
|
self.get_cmd = get_cmd
|
||||||
self.set_cmd = set_cmd
|
self.set_cmd = set_cmd
|
||||||
|
|
@ -370,47 +471,53 @@ class QueriedProperty(object):
|
||||||
self.ack_writes = ack_writes
|
self.ack_writes = ack_writes
|
||||||
self.__doc__ = doc
|
self.__doc__ = doc
|
||||||
|
|
||||||
|
|
||||||
# TODO: standardise the return (single value only vs parsed result), consider bool
|
# TODO: standardise the return (single value only vs parsed result), consider bool
|
||||||
def __get__(self, obj, objtype=None):
|
def __get__(self, obj, objtype=None):
|
||||||
if issubclass(type(obj),OptionalModule):
|
if issubclass(type(obj), OptionalModule):
|
||||||
obj.confirm_available()
|
obj.confirm_available()
|
||||||
obj=obj._parent
|
obj = obj._parent
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self
|
return self
|
||||||
assert issubclass(type(obj),ExtensibleSerialInstrument)
|
assert issubclass(type(obj), ExtensibleSerialInstrument)
|
||||||
if self.get_cmd is None:
|
if self.get_cmd is None:
|
||||||
raise AttributeError("unreadable attribute")
|
raise AttributeError("unreadable attribute")
|
||||||
# Allow certain "magic" values to set the response string
|
# Allow certain "magic" values to set the response string
|
||||||
for key, val in [('float',r"%f"),
|
for key, val in [("float", r"%f"), ("int", r"%d")]:
|
||||||
('int',r"%d"),]:
|
|
||||||
if self.response_string == key:
|
if self.response_string == key:
|
||||||
self.response_string = val
|
self.response_string = val
|
||||||
if self.response_string in ['bool', 'raw', None]:
|
if self.response_string in ["bool", "raw", None]:
|
||||||
value = obj.query(self.get_cmd)
|
value = obj.query(self.get_cmd)
|
||||||
if self.response_string == 'bool':
|
if self.response_string == "bool":
|
||||||
value = bool(value)
|
value = bool(value)
|
||||||
else:
|
else:
|
||||||
value = obj.parsed_query(self.get_cmd, self.response_string)
|
value = obj.parsed_query(self.get_cmd, self.response_string)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
def __set__(self, obj, value):
|
||||||
if issubclass(type(obj),OptionalModule):
|
if issubclass(type(obj), OptionalModule):
|
||||||
obj.confirm_available()
|
obj.confirm_available()
|
||||||
obj=obj._parent
|
obj = obj._parent
|
||||||
assert issubclass(type(obj),ExtensibleSerialInstrument)
|
assert issubclass(type(obj), ExtensibleSerialInstrument)
|
||||||
if self.set_cmd is None:
|
if self.set_cmd is None:
|
||||||
raise AttributeError("can't set attribute")
|
raise AttributeError("can't set attribute")
|
||||||
if self.validate is not None:
|
if self.validate is not None:
|
||||||
if value not in self.validate:
|
if value not in self.validate:
|
||||||
raise ValueError('invalid value supplied - value must be one of {}'.format(self.validate))
|
raise ValueError(
|
||||||
|
"invalid value supplied - value must be one of {}".format(
|
||||||
|
self.validate
|
||||||
|
)
|
||||||
|
)
|
||||||
if self.valrange is not None:
|
if self.valrange is not None:
|
||||||
if value < min(self.valrange) or value > max(self.valrange):
|
if value < min(self.valrange) or value > max(self.valrange):
|
||||||
raise ValueError('invalid value supplied - value must be in the range {}-{}'.format(*self.valrange))
|
raise ValueError(
|
||||||
|
"invalid value supplied - value must be in the range {}-{}".format(
|
||||||
|
*self.valrange
|
||||||
|
)
|
||||||
|
)
|
||||||
message = self.set_cmd
|
message = self.set_cmd
|
||||||
if '{0' in message:
|
if "{0" in message:
|
||||||
message = message.format(value)
|
message = message.format(value)
|
||||||
elif '%' in message:
|
elif "%" in message:
|
||||||
message = message % value
|
message = message % value
|
||||||
obj.write(message)
|
obj.write(message)
|
||||||
if self.ack_writes == "readline":
|
if self.ack_writes == "readline":
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,20 @@ It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function, division
|
from __future__ import print_function, division
|
||||||
import time
|
import time
|
||||||
from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE
|
from .extensible_serial_instrument import (
|
||||||
|
ExtensibleSerialInstrument,
|
||||||
|
OptionalModule,
|
||||||
|
QueriedProperty,
|
||||||
|
EIGHTBITS,
|
||||||
|
PARITY_NONE,
|
||||||
|
STOPBITS_ONE,
|
||||||
|
)
|
||||||
import serial, serial.tools.list_ports
|
import serial, serial.tools.list_ports
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class Sangaboard(ExtensibleSerialInstrument):
|
class Sangaboard(ExtensibleSerialInstrument):
|
||||||
"""Class managing serial communications with a Sangaboard
|
"""Class managing serial communications with a Sangaboard
|
||||||
|
|
||||||
|
|
@ -39,34 +47,45 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# List of valid and deprecated firmwares, for communication checks
|
# List of valid and deprecated firmwares, for communication checks
|
||||||
valid_firmwares = [
|
valid_firmwares = [(0, 5), (0, 4)]
|
||||||
(0, 5),
|
deprecated_firmwares = [(0, 4)]
|
||||||
(0, 4)
|
|
||||||
]
|
|
||||||
deprecated_firmwares = [
|
|
||||||
(0, 4)
|
|
||||||
]
|
|
||||||
|
|
||||||
# These are the settings for the sangaboards serial port, and can usually be left as default.
|
# These are the settings for the sangaboards serial port, and can usually be left as default.
|
||||||
port_settings = {'baudrate':115200, 'bytesize':EIGHTBITS, 'parity':PARITY_NONE, 'stopbits':STOPBITS_ONE}
|
port_settings = {
|
||||||
|
"baudrate": 115200,
|
||||||
|
"bytesize": EIGHTBITS,
|
||||||
|
"parity": PARITY_NONE,
|
||||||
|
"stopbits": STOPBITS_ONE,
|
||||||
|
}
|
||||||
|
|
||||||
# position, step time and ramp time are get/set using simple serial commands.
|
# position, step time and ramp time are get/set using simple serial commands.
|
||||||
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d",
|
position = QueriedProperty(
|
||||||
doc="Get the position of the axes as a tuple of 3 integers.")
|
get_cmd="p?",
|
||||||
step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d",
|
response_string=r"%d %d %d",
|
||||||
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
|
doc="Get the position of the axes as a tuple of 3 integers.",
|
||||||
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
|
)
|
||||||
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
|
step_time = QueriedProperty(
|
||||||
ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d",
|
get_cmd="dt?",
|
||||||
doc="Get or set the acceleration time in microseconds.\n\n"
|
set_cmd="dt %d",
|
||||||
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
|
response_string="minimum step delay %d",
|
||||||
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
|
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
|
||||||
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
|
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
|
||||||
"will be the same, but the motor will never reach full speed. It is saved to EEPROM on "
|
"the sangaboard, so it will be persistent even if the motor controller is turned off.",
|
||||||
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
|
)
|
||||||
|
ramp_time = QueriedProperty(
|
||||||
|
get_cmd="ramp_time?",
|
||||||
|
set_cmd="ramp_time %d",
|
||||||
|
response_string="ramp time %d",
|
||||||
|
doc="Get or set the acceleration time in microseconds.\n\n"
|
||||||
|
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
|
||||||
|
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
|
||||||
|
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
|
||||||
|
"will be the same, but the motor will never reach full speed. It is saved to EEPROM on "
|
||||||
|
"the sangaboard, so it will be persistent even if the motor controller is turned off.",
|
||||||
|
)
|
||||||
|
|
||||||
# The names of the sangaboard's axes. NB this also defines the number of axes
|
# The names of the sangaboard's axes. NB this also defines the number of axes
|
||||||
axis_names = ('x', 'y', 'z')
|
axis_names = ("x", "y", "z")
|
||||||
|
|
||||||
# Once initialised, `firmware` is a string that identifies the firmware version
|
# Once initialised, `firmware` is a string that identifies the firmware version
|
||||||
firmware = None
|
firmware = None
|
||||||
|
|
@ -88,30 +107,38 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
# Make absolutely sure that whatever port we're on is valid
|
# Make absolutely sure that whatever port we're on is valid
|
||||||
self.check_valid_firmware()
|
self.check_valid_firmware()
|
||||||
|
|
||||||
#Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
|
# Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
|
||||||
self.light_sensor = LightSensor(False)
|
self.light_sensor = LightSensor(False)
|
||||||
|
|
||||||
for module in self.list_modules():
|
for module in self.list_modules():
|
||||||
module_type=module.split(':')[0].strip()
|
module_type = module.split(":")[0].strip()
|
||||||
module_model=module.split(':')[1].strip()
|
module_model = module.split(":")[1].strip()
|
||||||
if module_type.startswith('Light Sensor'):
|
if module_type.startswith("Light Sensor"):
|
||||||
if module_model in self.supported_light_sensors:
|
if module_model in self.supported_light_sensors:
|
||||||
self.light_sensor = LightSensor(True,parent=self,model=module_model)
|
self.light_sensor = LightSensor(
|
||||||
|
True, parent=self, model=module_model
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning("Light sensor model \"%s\" not recognised."%(module_model))
|
logging.warning(
|
||||||
elif module_type.startswith('Endstops'):
|
'Light sensor model "%s" not recognised.' % (module_model)
|
||||||
|
)
|
||||||
|
elif module_type.startswith("Endstops"):
|
||||||
self.endstops = Endstops(True, parent=self, model=module_model)
|
self.endstops = Endstops(True, parent=self, model=module_model)
|
||||||
else:
|
else:
|
||||||
logging.warning("Module type \"{}\" not recognised.".format(module_type))
|
logging.warning(
|
||||||
|
'Module type "{}" not recognised.'.format(module_type)
|
||||||
|
)
|
||||||
|
|
||||||
self.board = self.query("board",timeout=2).rstrip()
|
self.board = self.query("board", timeout=2).rstrip()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If an error occurred while setting up (e.g. because the board isn't connected or something)
|
# If an error occurred while setting up (e.g. because the board isn't connected or something)
|
||||||
# make sure we close the serial port cleanly (otherwise it hangs open).
|
# make sure we close the serial port cleanly (otherwise it hangs open).
|
||||||
self.close()
|
self.close()
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
logging.error("You may need to update the firmware running on the Sangaboard.")
|
logging.error(
|
||||||
|
"You may need to update the firmware running on the Sangaboard."
|
||||||
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def test_communications(self):
|
def test_communications(self):
|
||||||
|
|
@ -124,16 +151,22 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
logging.debug("Running firmware checks")
|
logging.debug("Running firmware checks")
|
||||||
|
|
||||||
# Request firmware version from the board
|
# Request firmware version from the board
|
||||||
self.firmware = self.query("version",timeout=2).rstrip()
|
self.firmware = self.query("version", timeout=2).rstrip()
|
||||||
logging.info("Firmware response: {}".format(self.firmware))
|
logging.info("Firmware response: {}".format(self.firmware))
|
||||||
# Check for valid firmware string
|
# Check for valid firmware string
|
||||||
if self.firmware:
|
if self.firmware:
|
||||||
match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware)
|
match = re.match(
|
||||||
|
r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware
|
||||||
|
)
|
||||||
if not match:
|
if not match:
|
||||||
# Try old firmware format
|
# Try old firmware format
|
||||||
match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware)
|
match = re.match(
|
||||||
|
r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware
|
||||||
|
)
|
||||||
if not match:
|
if not match:
|
||||||
logging.error("Version string \"{}\" not recognised.".format(self.firmware))
|
logging.error(
|
||||||
|
'Version string "{}" not recognised.'.format(self.firmware)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logging.error("No firmware version string was returned.")
|
logging.error("No firmware version string was returned.")
|
||||||
|
|
@ -146,7 +179,9 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
self.firmware_version = match.group(1)
|
self.firmware_version = match.group(1)
|
||||||
|
|
||||||
if version_tuple not in Sangaboard.valid_firmwares:
|
if version_tuple not in Sangaboard.valid_firmwares:
|
||||||
logging.error("This version of the Python module requires firmware v0.5 (with legacy support for v0.4)")
|
logging.error(
|
||||||
|
"This version of the Python module requires firmware v0.5 (with legacy support for v0.4)"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if version_tuple in Sangaboard.deprecated_firmwares:
|
if version_tuple in Sangaboard.deprecated_firmwares:
|
||||||
|
|
@ -164,10 +199,12 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
axis: None (for 3-axis moves) or one of 'x','y','z'
|
axis: None (for 3-axis moves) or one of 'x','y','z'
|
||||||
"""
|
"""
|
||||||
if axis is not None:
|
if axis is not None:
|
||||||
assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names)
|
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||||
|
self.axis_names
|
||||||
|
)
|
||||||
self.query("mr{} {}".format(axis, int(displacement)))
|
self.query("mr{} {}".format(axis, int(displacement)))
|
||||||
else:
|
else:
|
||||||
#TODO: assert displacement is 3 integers
|
# TODO: assert displacement is 3 integers
|
||||||
self.query("mr {} {} {}".format(*list(displacement)))
|
self.query("mr {} {} {}".format(*list(displacement)))
|
||||||
|
|
||||||
def release_motors(self):
|
def release_motors(self):
|
||||||
|
|
@ -181,12 +218,12 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
queries the board for its position, then instructs it to make about
|
queries the board for its position, then instructs it to make about
|
||||||
relative move.
|
relative move.
|
||||||
"""
|
"""
|
||||||
rel_mov = [f_pos-i_pos for f_pos,i_pos in zip(final,self.position)]
|
rel_mov = [f_pos - i_pos for f_pos, i_pos in zip(final, self.position)]
|
||||||
return self.move_rel(rel_mov, **kwargs)
|
return self.move_rel(rel_mov, **kwargs)
|
||||||
|
|
||||||
def query(self, message, *args, **kwargs):
|
def query(self, message, *args, **kwargs):
|
||||||
"""Send a message and read the response. See ExtensibleSerialInstrument.query()"""
|
"""Send a message and read the response. See ExtensibleSerialInstrument.query()"""
|
||||||
time.sleep(0.001) # This is to protect the stage from us talking too fast!
|
time.sleep(0.001) # This is to protect the stage from us talking too fast!
|
||||||
return ExtensibleSerialInstrument.query(self, message, *args, **kwargs)
|
return ExtensibleSerialInstrument.query(self, message, *args, **kwargs)
|
||||||
|
|
||||||
def list_modules(self):
|
def list_modules(self):
|
||||||
|
|
@ -194,12 +231,15 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
|
|
||||||
Each module will correspond to a string of the form ``Module Name: Model``
|
Each module will correspond to a string of the form ``Module Name: Model``
|
||||||
"""
|
"""
|
||||||
modules = self.query("list_modules",multiline=True,termination_line="--END--\r\n").split('\r\n')[:-2]
|
modules = self.query(
|
||||||
|
"list_modules", multiline=True, termination_line="--END--\r\n"
|
||||||
|
).split("\r\n")[:-2]
|
||||||
return [str(module) for module in modules]
|
return [str(module) for module in modules]
|
||||||
|
|
||||||
def print_help(self):
|
def print_help(self):
|
||||||
"""Print the stage's built-in help message."""
|
"""Print the stage's built-in help message."""
|
||||||
print(self.query("help",multiline=True,termination_line="--END--\r\n"))
|
print(self.query("help", multiline=True, termination_line="--END--\r\n"))
|
||||||
|
|
||||||
|
|
||||||
class LightSensor(OptionalModule):
|
class LightSensor(OptionalModule):
|
||||||
"""An optional module giving access to the light sensor.
|
"""An optional module giving access to the light sensor.
|
||||||
|
|
@ -209,19 +249,25 @@ class LightSensor(OptionalModule):
|
||||||
module which is an instance of this class. It can be used to access
|
module which is an instance of this class. It can be used to access
|
||||||
the light sensor (usually via the I2C bus).
|
the light sensor (usually via the I2C bus).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
valid_gains = None
|
valid_gains = None
|
||||||
_valid_gains_int = None
|
_valid_gains_int = None
|
||||||
integration_time = QueriedProperty(
|
integration_time = QueriedProperty(
|
||||||
get_cmd="light_sensor_integration_time?",
|
get_cmd="light_sensor_integration_time?",
|
||||||
set_cmd="light_sensor_integration_time %d",
|
set_cmd="light_sensor_integration_time %d",
|
||||||
response_string="light sensor integration time %d ms",
|
response_string="light sensor integration time %d ms",
|
||||||
doc="Get or set the integration time of the light sensor in milliseconds."
|
doc="Get or set the integration time of the light sensor in milliseconds.",
|
||||||
|
)
|
||||||
|
intensity = QueriedProperty(
|
||||||
|
get_cmd="light_sensor_intensity?",
|
||||||
|
response_string="%d",
|
||||||
|
doc="Read the current intensity measured by the light sensor (arbitrary units).",
|
||||||
)
|
)
|
||||||
intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d",
|
|
||||||
doc="Read the current intensity measured by the light sensor (arbitrary units).")
|
|
||||||
|
|
||||||
def __init__(self,available,parent=None,model="Generic"):
|
def __init__(self, available, parent=None, model="Generic"):
|
||||||
super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model)
|
super(LightSensor, self).__init__(
|
||||||
|
available, parent=parent, module_type="LightSensor", model=model
|
||||||
|
)
|
||||||
if available:
|
if available:
|
||||||
self.valid_gains = self.__get_gain_values()
|
self.valid_gains = self.__get_gain_values()
|
||||||
self._valid_gains_int = [int(g) for g in self.valid_gains]
|
self._valid_gains_int = [int(g) for g in self.valid_gains]
|
||||||
|
|
@ -232,21 +278,25 @@ class LightSensor(OptionalModule):
|
||||||
|
|
||||||
Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers."""
|
Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers."""
|
||||||
self.confirm_available()
|
self.confirm_available()
|
||||||
gain = self._parent.query('light_sensor_gain?')
|
gain = self._parent.query("light_sensor_gain?")
|
||||||
M = re.search('[0-9\.]+(?=x)',gain)
|
M = re.search("[0-9\.]+(?=x)", gain)
|
||||||
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
|
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
|
||||||
#gain is a float as non integer gains exist but are set with floor of value
|
# gain is a float as non integer gains exist but are set with floor of value
|
||||||
return float(M.group())
|
return float(M.group())
|
||||||
|
|
||||||
@gain.setter
|
@gain.setter
|
||||||
def gain(self,val):
|
def gain(self, val):
|
||||||
self.confirm_available()
|
self.confirm_available()
|
||||||
assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains)
|
assert (
|
||||||
gain = self._parent.query('light_sensor_gain %d'%(int(val)))
|
int(val) in self._valid_gains_int
|
||||||
M = re.search('[0-9\.]+(?=x)',gain)
|
), "Gain {} not valid must be one of: {}".format(val, self.valid_gains)
|
||||||
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
|
gain = self._parent.query("light_sensor_gain %d" % (int(val)))
|
||||||
#gain is a float as non integer gains exist but are set with floor of value
|
M = re.search("[0-9\.]+(?=x)", gain)
|
||||||
assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,gain)
|
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
|
||||||
|
# gain is a float as non integer gains exist but are set with floor of value
|
||||||
|
assert int(val) == int(
|
||||||
|
float(M.group())
|
||||||
|
), 'Gain of {} set, "{}" returned'.format(val, gain)
|
||||||
|
|
||||||
def __get_gain_values(self):
|
def __get_gain_values(self):
|
||||||
"""Read the allowable values for the light sensor's gain.
|
"""Read the allowable values for the light sensor's gain.
|
||||||
|
|
@ -256,15 +306,16 @@ class LightSensor(OptionalModule):
|
||||||
values, the list will be of strings.
|
values, the list will be of strings.
|
||||||
"""
|
"""
|
||||||
self.confirm_available()
|
self.confirm_available()
|
||||||
gains = self._parent.query('light_sensor_gain_values?')
|
gains = self._parent.query("light_sensor_gain_values?")
|
||||||
try:
|
try:
|
||||||
M = re.findall('[0-9\.]+(?=x)',gains)
|
M = re.findall("[0-9\.]+(?=x)", gains)
|
||||||
return [float(gain) for gain in M]
|
return [float(gain) for gain in M]
|
||||||
except:
|
except:
|
||||||
# Fall back to strings if we don't get floats (unlikely)
|
# Fall back to strings if we don't get floats (unlikely)
|
||||||
gain_strings = gains[20:].split(", ")
|
gain_strings = gains[20:].split(", ")
|
||||||
return gain_strings
|
return gain_strings
|
||||||
|
|
||||||
|
|
||||||
class Endstops(OptionalModule):
|
class Endstops(OptionalModule):
|
||||||
"""An optional module for use with endstops.
|
"""An optional module for use with endstops.
|
||||||
|
|
||||||
|
|
@ -273,35 +324,41 @@ class Endstops(OptionalModule):
|
||||||
the type, state of the endstops, read and write maximum positions, and home.
|
the type, state of the endstops, read and write maximum positions, and home.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
installed=[]
|
installed = []
|
||||||
"""List of installed endstop types (min, max, soft)"""
|
"""List of installed endstop types (min, max, soft)"""
|
||||||
|
|
||||||
def __init__(self,available,parent=None,model="min"):
|
def __init__(self, available, parent=None, model="min"):
|
||||||
super(Endstops, self).__init__(available,parent=parent,model="Endstops")
|
super(Endstops, self).__init__(available, parent=parent, model="Endstops")
|
||||||
self.installed=model.split(' ')
|
self.installed = model.split(" ")
|
||||||
|
|
||||||
status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d",
|
status = QueriedProperty(
|
||||||
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis")
|
get_cmd="endstops?",
|
||||||
maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d",
|
response_string=r"%d %d %d",
|
||||||
doc="Vector of maximum positions, homing to max endstops will measure this, "+
|
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis",
|
||||||
"can be set to a known value for use with max only and min+soft endstops")
|
)
|
||||||
|
maxima = QueriedProperty(
|
||||||
|
get_cmd="max_p?",
|
||||||
|
set_cmd="max %d %d %d",
|
||||||
|
response_string="%d %d %d",
|
||||||
|
doc="Vector of maximum positions, homing to max endstops will measure this, "
|
||||||
|
+ "can be set to a known value for use with max only and min+soft endstops",
|
||||||
|
)
|
||||||
|
|
||||||
def home(self, direction="min", axes=['x','y','z']):
|
def home(self, direction="min", axes=["x", "y", "z"]):
|
||||||
""" Home given/all axes in the given direction (min/max/both)
|
""" Home given/all axes in the given direction (min/max/both)
|
||||||
|
|
||||||
:param direction: one of {min,max,both}
|
:param direction: one of {min,max,both}
|
||||||
:param axes: list of axes e.g. ['x','y']
|
:param axes: list of axes e.g. ['x','y']
|
||||||
"""
|
"""
|
||||||
ax=0
|
ax = 0
|
||||||
if 'x' in axes:
|
if "x" in axes:
|
||||||
ax+=1
|
ax += 1
|
||||||
if 'y' in axes:
|
if "y" in axes:
|
||||||
ax+=2
|
ax += 2
|
||||||
if 'z' in axes:
|
if "z" in axes:
|
||||||
ax+=3
|
ax += 3
|
||||||
|
|
||||||
if direction == "min" or direction == "both":
|
if direction == "min" or direction == "both":
|
||||||
self._parent.query('home_min {}'.format(ax))
|
self._parent.query("home_min {}".format(ax))
|
||||||
if direction == "max" or direction == "both":
|
if direction == "max" or direction == "both":
|
||||||
self._parent.query('home_max {}'.format(ax))
|
self._parent.query("home_max {}".format(ax))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class TaskOrchestrator:
|
||||||
tasks (list): List of `Task` objects
|
tasks (list): List of `Task` objects
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.tasks = []
|
self.tasks = []
|
||||||
|
|
||||||
|
|
@ -106,6 +107,7 @@ class Task:
|
||||||
`status` will be one of 'idle'', 'running', 'error', or 'success'.
|
`status` will be one of 'idle'', 'running', 'error', or 'success'.
|
||||||
`return` eventually holds the return value of the task function.
|
`return` eventually holds the return value of the task function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, function, *args, **kwargs):
|
def __init__(self, function, *args, **kwargs):
|
||||||
# The task long-running method
|
# The task long-running method
|
||||||
self.task = function
|
self.task = function
|
||||||
|
|
@ -117,39 +119,41 @@ class Task:
|
||||||
self.id = uuid.uuid4().hex
|
self.id = uuid.uuid4().hex
|
||||||
|
|
||||||
self.state = {
|
self.state = {
|
||||||
'id': self.id,
|
"id": self.id,
|
||||||
'status': 'idle',
|
"status": "idle",
|
||||||
'return': None,
|
"return": None,
|
||||||
'start_time': None,
|
"start_time": None,
|
||||||
'end_time': None,
|
"end_time": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def process(self, f):
|
def process(self, f):
|
||||||
"""
|
"""
|
||||||
Wraps the target function to handle recording `status` and `return` to `state`.
|
Wraps the target function to handle recording `status` and `return` to `state`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
self.state['status'] = 'running'
|
self.state["status"] = "running"
|
||||||
try:
|
try:
|
||||||
r = f(*args, **kwargs)
|
r = f(*args, **kwargs)
|
||||||
s = 'success'
|
s = "success"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
logging.error(traceback.format_exc())
|
logging.error(traceback.format_exc())
|
||||||
r = str(e)
|
r = str(e)
|
||||||
s = 'error'
|
s = "error"
|
||||||
self.state['return'] = r
|
self.state["return"] = r
|
||||||
self.state['status'] = s
|
self.state["status"] = s
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Records the task start and end times to `state`, and runs the task wrapped in `process`.
|
Records the task start and end times to `state`, and runs the task wrapped in `process`.
|
||||||
"""
|
"""
|
||||||
self.state['start_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
self.state["start_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||||
self.process(self.task)(*self.args, **self.kwargs)
|
self.process(self.task)(*self.args, **self.kwargs)
|
||||||
self._running = False
|
self._running = False
|
||||||
self.state['end_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
self.state["end_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||||
|
|
||||||
def start(self): # Start and draw
|
def start(self): # Start and draw
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ def set_properties(obj, **kwargs):
|
||||||
try:
|
try:
|
||||||
saved_properties[k] = getattr(obj, k)
|
saved_properties[k] = getattr(obj, k)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
print("Warning: could not get {} on {}. This property will not be restored!".format(k, obj))
|
print(
|
||||||
|
"Warning: could not get {} on {}. This property will not be restored!".format(
|
||||||
|
k, obj
|
||||||
|
)
|
||||||
|
)
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
setattr(obj, k, v)
|
setattr(obj, k, v)
|
||||||
try:
|
try:
|
||||||
|
|
@ -27,12 +31,14 @@ def set_properties(obj, **kwargs):
|
||||||
setattr(obj, k, v)
|
setattr(obj, k, v)
|
||||||
|
|
||||||
|
|
||||||
def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None, asint=True):
|
def axes_to_array(
|
||||||
|
coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True
|
||||||
|
):
|
||||||
"""Takes key-value pairs of a JSON value, and maps onto an array"""
|
"""Takes key-value pairs of a JSON value, and maps onto an array"""
|
||||||
# If no base array is given
|
# If no base array is given
|
||||||
if not base_array:
|
if not base_array:
|
||||||
# Create an array of zeros
|
# Create an array of zeros
|
||||||
base_array = [0]*len(axis_keys)
|
base_array = [0] * len(axis_keys)
|
||||||
else:
|
else:
|
||||||
# Create a copy of the passed base_array
|
# Create a copy of the passed base_array
|
||||||
base_array = copy.copy(base_array)
|
base_array = copy.copy(base_array)
|
||||||
|
|
@ -40,7 +46,9 @@ def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=N
|
||||||
# Do the mapping
|
# Do the mapping
|
||||||
for axis, key in enumerate(axis_keys):
|
for axis, key in enumerate(axis_keys):
|
||||||
if key in coordinate_dictionary:
|
if key in coordinate_dictionary:
|
||||||
base_array[axis] = int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
|
base_array[axis] = (
|
||||||
|
int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
|
||||||
|
)
|
||||||
|
|
||||||
return base_array
|
return base_array
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue