Merge branch 'code-clean-sept19' into 'master'

Code clean sept19

See merge request openflexure/openflexure-microscope-server!34
This commit is contained in:
Joel Collins 2019-10-05 13:11:26 +00:00
commit 019b33ced9
70 changed files with 2305 additions and 1962 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
# Pyenv files
.python-version
.venv/
.venv*/
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -17,32 +17,34 @@ import sys
# Load module from relative imports
module_path = os.path.abspath('../..')
module_path = os.path.abspath("../..")
sys.path.insert(0, module_path)
# Handle mock imports for non-platform-agnostic modules
from unittest.mock import MagicMock
class Mock(MagicMock):
@classmethod
def __getattr__(cls, name):
return MagicMock()
mock_imports = ['picamera', 'picamera.array', 'picamera.mmalobj']
mock_imports = ["picamera", "picamera.array", "picamera.mmalobj"]
sys.modules.update((mod_name, Mock()) for mod_name in mock_imports)
# -- Project information -----------------------------------------------------
project = 'OpenFlexure Microscope Software'
copyright = '2018, Bath Open Instrumentation Group'
author = 'Bath Open Instrumentation Group'
project = "OpenFlexure Microscope Software"
copyright = "2018, Bath Open Instrumentation Group"
author = "Bath Open Instrumentation Group"
# The short X.Y version
version = ''
version = ""
# The full version, including alpha/beta/rc tags
release = ''
release = ""
# -- General configuration ---------------------------------------------------
@ -55,32 +57,32 @@ release = ''
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinx.ext.ifconfig',
'sphinxcontrib.httpdomain',
'sphinxcontrib.autohttp.flask',
'sphinxcontrib.autohttp.flaskqref'
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"sphinx.ext.ifconfig",
"sphinxcontrib.httpdomain",
"sphinxcontrib.autohttp.flask",
"sphinxcontrib.autohttp.flaskqref",
]
# Override ordering
autodoc_member_order = 'bysource'
autodoc_member_order = "bysource"
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -114,7 +116,7 @@ html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
@ -130,7 +132,7 @@ html_static_path = ['_static']
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'OpenFlexureMicroscopeSoftwaredoc'
htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc"
# -- Options for LaTeX output ------------------------------------------------
@ -139,15 +141,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -157,8 +156,13 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'OpenFlexureMicroscopeSoftware.tex', 'OpenFlexure Microscope Software Documentation',
'Bath Open Instrumentation Group', 'manual'),
(
master_doc,
"OpenFlexureMicroscopeSoftware.tex",
"OpenFlexure Microscope Software Documentation",
"Bath Open Instrumentation Group",
"manual",
)
]
@ -167,8 +171,13 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'openflexuremicroscopesoftware', 'OpenFlexure Microscope Software Documentation',
[author], 1)
(
master_doc,
"openflexuremicroscopesoftware",
"OpenFlexure Microscope Software Documentation",
[author],
1,
)
]
@ -178,9 +187,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'OpenFlexureMicroscopeSoftware', 'OpenFlexure Microscope Software Documentation',
author, 'OpenFlexureMicroscopeSoftware', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"OpenFlexureMicroscopeSoftware",
"OpenFlexure Microscope Software Documentation",
author,
"OpenFlexureMicroscopeSoftware",
"One line description of project.",
"Miscellaneous",
)
]
@ -199,7 +214,7 @@ epub_title = project
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
@ -208,11 +223,11 @@ epub_exclude_files = ['search.html']
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'openflexure_stage': ('https://openflexure-stage.readthedocs.io/en/latest/', None),
'picamera': ('https://picamera.readthedocs.io/en/release-1.13/', None)
}
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
"picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None),
}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
todo_include_todos = True

View file

@ -13,6 +13,7 @@ FORM_PATH = os.path.join(HERE, "forms.json")
### MICROSCOPE PLUGIN ###
class MyPluginClass(MicroscopePlugin):
"""
A set of default plugins
@ -20,13 +21,13 @@ class MyPluginClass(MicroscopePlugin):
global FORM_PATH
with open(FORM_PATH, 'r') as sc:
with open(FORM_PATH, "r") as sc:
api_form = json.load(sc)
api_views = {
'/identify': IdentifyAPI,
'/hello': HelloWorldAPI,
'/timelapse': TimelapseAPI,
"/identify": IdentifyAPI,
"/hello": HelloWorldAPI,
"/timelapse": TimelapseAPI,
}
def identify(self):
@ -34,8 +35,9 @@ class MyPluginClass(MicroscopePlugin):
Demonstrate access to Microscope.camera, and Microscope.stage
"""
response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera,
self.microscope.stage)
response = "My parent camera is {}, and my parent stage is {}.".format(
self.microscope.camera, self.microscope.stage
)
return response
def hello_world(self):
@ -57,34 +59,34 @@ class MyPluginClass(MicroscopePlugin):
for _ in range(n_images):
# Create a data stream to capture to
capture_data = self.microscope.camera.new_image(
write_to_file=True,
temporary=False)
output = self.microscope.camera.new_image(
temporary=False
)
# Capture a still image from the Pi camera, into the data stream
self.microscope.camera.capture(
capture_data,
use_video_port=True)
self.microscope.camera.capture(output.file, use_video_port=True)
# Append the capture data to our list
capture_array.append(capture_data)
capture_array.append(output)
# Wait for 1 minute
time.sleep(60)
time.sleep(60)
### API VIEWS ###
class IdentifyAPI(MicroscopeViewPlugin):
"""
A simple example API plugin, attached through the main microscope plugin.
"""
def get(self):
"""
Method to call when an HTTP GET request is made.
"""
# Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
data = self.plugin.identify()
data = self.plugin.identify()
return Response(escape(data))
@ -99,13 +101,11 @@ class HelloWorldAPI(MicroscopeViewPlugin):
"""
# If the microscope does not already contain our plugin_string attribute
if not hasattr(self.microscope, 'plugin_string'):
if not hasattr(self.microscope, "plugin_string"):
# Make a string, using the MicroscopeViewPlugin.plugin shortcut
self.microscope.plugin_string = self.plugin.hello_world()
json_response = jsonify({
'plugin_string': self.microscope.plugin_string
})
json_response = jsonify({"plugin_string": self.microscope.plugin_string})
return Response(json_response)
@ -118,18 +118,17 @@ class HelloWorldAPI(MicroscopeViewPlugin):
payload = JsonResponse(request)
# Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
new_plugin_string = payload.param('plugin_string', default='', convert=str)
new_plugin_string = payload.param("plugin_string", default="", convert=str)
if new_plugin_string: # If not None or empty
# Set microscope attribute to the specified string
self.microscope.plugin_string = new_plugin_string
self.microscope.plugin_string = new_plugin_string
json_response = jsonify({
'plugin_string': self.microscope.plugin_string
})
json_response = jsonify({"plugin_string": self.microscope.plugin_string})
return Response(json_response)
class TimelapseAPI(MicroscopeViewPlugin):
def post(self):
@ -137,10 +136,12 @@ class TimelapseAPI(MicroscopeViewPlugin):
payload = JsonResponse(request)
# Extract the "n_images" parameter if it was passed. Otherwise, default to 10.
n_images = payload.param('n_images', default=10, convert=int)
n_images = payload.param("n_images", default=10, convert=int)
# Attach the long-running method as a microscope task
self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images)
self.timelapse_task = self.microscope.task.start(
self.plugin.timelapse, n_images
)
# Return the state of the task (will show ID, start time, and status before the task has finished)
return jsonify(self.timelapse_task.state), 202
return jsonify(self.timelapse_task.state), 202

View file

@ -103,17 +103,16 @@ For example, a timelapse plugin may look like:
for _ in range(n_images):
# Create a data stream to capture to
capture_data = self.microscope.camera.new_image(
write_to_file=True,
output = self.microscope.camera.new_image(
temporary=False)
# Capture a still image from the Pi camera, into the data stream
self.microscope.camera.capture(
capture_data,
output.file,
use_video_port=True)
# Append the capture data to our list
capture_array.append(capture_data)
capture_array.append(output)
# Wait for 1 minute
time.sleep(60)

View file

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

View file

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

View file

@ -6,8 +6,7 @@ import logging
import sys
import os
from flask import (
Flask, jsonify, send_file)
from flask import Flask, jsonify, send_file
from serial import SerialException
from datetime import datetime
@ -39,27 +38,24 @@ from openflexure_microscope.stage.mock import MockStage
# Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, 'openflexure_microscope.log')
DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, "openflexure_microscope.log")
if (__name__ == "__main__") or (not is_gunicorn):
# If imported, but not by gunicorn
print("Letting sys handle logs")
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
else:
# Direct standard Python logging to file and console
root = logging.getLogger()
error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s")
rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE,
maxBytes=1000000,
backupCount=7
error_formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
error_handlers = [
rotating_logfile,
logging.StreamHandler()
]
rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7
)
error_handlers = [rotating_logfile, logging.StreamHandler()]
for handler in error_handlers:
handler.setFormatter(error_formatter)
@ -70,10 +66,33 @@ else:
# Create a dummy microscope object, with no hardware attachments
api_microscope = Microscope()
# Rebuild the capture list
# TODO: Offload to a thread?
stored_image_list = build_captures_from_exif()
# Initialise camera
logging.debug("Creating camera object...")
try:
api_camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No valid camera hardware found. Falling back to mock camera!")
api_camera = MockStreamer()
# Initialise stage
logging.debug("Creating stage object...")
try:
api_stage = SangaStage()
except Exception as e:
logging.error(e)
logging.warning("No valid stage hardware found. Falling back to mock stage!")
api_stage = MockStage()
# Attach devices to microscope
logging.debug("Attaching devices to microscope...")
api_microscope.attach(api_camera, api_stage)
# Restore loaded capture array to camera object
logging.debug("Restoring captures...")
api_microscope.camera.images = build_captures_from_exif(api_microscope.camera.paths["default"])
logging.debug("Microscope successfully attached!")
# Generate API URI based on version from filename
def uri(suffix, api_version, base=None):
@ -88,78 +107,35 @@ def uri(suffix, api_version, base=None):
app = Flask(__name__)
app.url_map.strict_slashes = False
CORS(app, resources=r'/api/*')
CORS(app, resources=r"*")
# Make errors more API friendly
handler = JSONExceptionHandler(app)
# After app starts, but before first request, attach hardware to global microscope
@app.before_first_request
def attach_microscope():
# Create the microscope object globally (common to all spawned server threads)
global api_microscope, stored_image_list
logging.debug("First request made. Populating microscope with hardware...")
# Initialise camera
logging.debug("Creating camera object...")
try:
api_camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No valid camera hardware found. Falling back to mock camera!")
api_camera = MockStreamer()
# Initialise stage
logging.debug("Creating stage object...")
try:
api_stage = SangaStage()
except Exception as e:
logging.error(e)
logging.warning("No valid stage hardware found. Falling back to mock stage!")
api_stage = MockStage()
# Attach devices to microscope
logging.debug("Attaching devices to microscope...")
api_microscope.attach(
api_camera,
api_stage
)
# Restore loaded capture array to camera object
logging.debug("Restoring captures...")
if stored_image_list:
api_microscope.camera.images = stored_image_list
logging.debug("Microscope successfully attached!")
# WEBAPP ROUTES
# API ROUTES
# Base routes
base_blueprint = blueprints.base.construct_blueprint(api_microscope)
app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1'))
app.register_blueprint(base_blueprint, url_prefix=uri("", "v1"))
# Stage routes
stage_blueprint = blueprints.stage.construct_blueprint(api_microscope)
app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1'))
app.register_blueprint(stage_blueprint, url_prefix=uri("/stage", "v1"))
# Camera routes
camera_blueprint = blueprints.camera.construct_blueprint(api_microscope)
app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1'))
app.register_blueprint(camera_blueprint, url_prefix=uri("/camera", "v1"))
# Plugin routes
plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope)
app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1'))
app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1"))
# Task routes
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1'))
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
@app.route('/routes')
@app.route("/routes")
def routes():
"""
List of all connected API routes
@ -173,7 +149,7 @@ def routes():
return jsonify(list_routes(app))
@app.route('/log')
@app.route("/log")
def err_log():
"""
Most recent 1mb of log output
@ -186,9 +162,9 @@ def err_log():
"""
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
return send_file(
DEFAULT_LOGFILE,
as_attachment=True,
attachment_filename='openflexure_microscope_{}.log'.format(timestamp)
DEFAULT_LOGFILE,
as_attachment=True,
attachment_filename="openflexure_microscope_{}.log".format(timestamp),
)
@ -219,4 +195,4 @@ def cleanup():
atexit.register(cleanup)
if __name__ == "__main__":
app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)

View file

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

View file

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

View file

@ -6,7 +6,6 @@ import logging
class StreamAPI(MicroscopeView):
def get(self):
"""
Real-time MJPEG stream from the microscope camera
@ -22,11 +21,30 @@ class StreamAPI(MicroscopeView):
return Response(
gen(self.microscope.camera),
mimetype='multipart/x-mixed-replace; boundary=frame')
mimetype="multipart/x-mixed-replace; boundary=frame",
)
class SnapshotAPI(MicroscopeView):
def get(self):
"""
Single snapshot from the camera stream
.. :quickref: State; Camera snapshot
:>header Accept: image/jpeg
:>header Content-Type: image/jpeg
:status 200: stream active
"""
# Restart stream worker thread
self.microscope.camera.start_worker()
return Response(
self.microscope.camera.get_frame(),
mimetype="image/jpeg",
)
class StateAPI(MicroscopeView):
def get(self):
"""
JSON representation of the microscope object.
@ -77,7 +95,6 @@ class StateAPI(MicroscopeView):
class ConfigAPI(MicroscopeView):
def get(self):
"""
JSON representation of the microscope config.
@ -216,21 +233,22 @@ class ConfigAPI(MicroscopeView):
def construct_blueprint(microscope_obj):
blueprint = Blueprint('base_blueprint', __name__)
blueprint = Blueprint("base_blueprint", __name__)
blueprint.add_url_rule(
'/stream',
view_func=StreamAPI.as_view('stream', microscope=microscope_obj)
"/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj)
)
blueprint.add_url_rule(
'/state',
view_func=StateAPI.as_view('state', microscope=microscope_obj)
"/snapshot", view_func=SnapshotAPI.as_view("snapshot", microscope=microscope_obj)
)
blueprint.add_url_rule(
'/config',
view_func=ConfigAPI.as_view('config', microscope=microscope_obj)
"/state", view_func=StateAPI.as_view("state", microscope=microscope_obj)
)
blueprint.add_url_rule(
"/config", view_func=ConfigAPI.as_view("config", microscope=microscope_obj)
)
return blueprint

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,25 +1,22 @@
# -*- coding: utf-8 -*-
import time
import os
import shutil
import threading
import datetime
import logging
from abc import ABCMeta, abstractmethod
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
from .capture import CaptureObject, BASE_CAPTURE_PATH, TEMP_CAPTURE_PATH
from .capture import CaptureObject
from openflexure_microscope.utilities import entry_by_id
from openflexure_microscope.lock import StrictLock
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
def last_entry(object_list: list):
"""Return the last entry of a list, if the list contains items."""
if object_list: # If any images have been captured
@ -45,23 +42,19 @@ def generate_numbered_basename(obj_list: list) -> str:
return basename
def shunt_captures(target_list: list):
for obj in target_list: # For each older capture
obj.shunt() # Shunt capture from memory to storage
class CameraEvent(object):
"""
A frame-signaller object used by any instances or subclasses of BaseCamera.
An event-like class that signals all active clients when a new frame is available.
"""
def __init__(self):
self.events = {}
def wait(self, timeout: int = 5):
"""Wait for the next frame (invoked from each client's thread)."""
ident = get_ident()
ident = threading.get_ident()
if ident not in self.events:
# this is a new client
# add an entry for it in the self.events dict
@ -91,7 +84,7 @@ class CameraEvent(object):
def clear(self):
"""Clear frame event, once processed."""
self.events[get_ident()][0].clear()
self.events[threading.get_ident()][0].clear()
class BaseCamera(metaclass=ABCMeta):
@ -112,9 +105,10 @@ class BaseCamera(metaclass=ABCMeta):
images (list): List of image capture objects
videos (list): List of video capture objects
"""
def __init__(self):
self.thread = None
self.camera = None
self.camera = None
self.lock = StrictLock(timeout=1)
@ -126,13 +120,15 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20
self.stream_timeout_enabled = False
self.state = {}
self.state = {
"board": None
}
# TODO: Load/save these to config
self.paths = {
'image': BASE_CAPTURE_PATH,
'video': BASE_CAPTURE_PATH,
'image_tmp': TEMP_CAPTURE_PATH,
'video_tpm': TEMP_CAPTURE_PATH
}
"default": BASE_CAPTURE_PATH,
"temp": TEMP_CAPTURE_PATH
}
# Capture data
self.images = []
@ -141,12 +137,16 @@ class BaseCamera(metaclass=ABCMeta):
@abstractmethod
def apply_config(self, config: dict):
"""Update settings from a config dictionary"""
pass
with self.lock:
# Apply valid config params to camera object
for key, value in config.items(): # For each provided setting
if hasattr(self, key): # If the instance has a matching property
setattr(self, key, value) # Set to the target value
@abstractmethod
def read_config(self) -> dict:
"""Return the current settings as a dictionary"""
pass
return {"paths": self.paths}
def __enter__(self):
"""Create camera on context enter."""
@ -163,10 +163,22 @@ class BaseCamera(metaclass=ABCMeta):
for capture_list in [self.images, self.videos]:
for stream_object in capture_list:
stream_object.close()
# Empty temp directory
self.clear_tmp()
# Stop worker thread
self.stop_worker()
logging.info("Closed {}".format(self))
def clear_tmp(self):
"""
Removes all files in the temporary capture directories
"""
if os.path.isdir(self.paths["temp"]):
logging.info("Clearing {}...".format(self.paths["temp"]))
shutil.rmtree(self.paths["temp"])
logging.debug("Cleared {}.".format(self.paths["temp"]))
# START AND STOP WORKER THREAD
def start_worker(self, timeout: int = 5) -> bool:
@ -176,7 +188,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time()
self.stop = False
if not self.state['stream_active']:
if not self.state["stream_active"]:
# start background frame thread
self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True
@ -196,12 +208,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout
if self.state['stream_active']:
if self.state["stream_active"]:
self.stop = True
self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting for stream thread to exit.")
while self.state['stream_active']:
while self.state["stream_active"]:
if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("Timeout waiting for worker thread close.")
@ -249,18 +261,17 @@ class BaseCamera(metaclass=ABCMeta):
# CREATING NEW CAPTURES
def new_image(
self,
write_to_file: bool = True,
temporary: bool = True,
filename: str = None,
folder: str = "",
fmt: str = 'jpeg'):
self,
temporary: bool = True,
filename: str = None,
folder: str = "",
fmt: str = "jpeg",
):
"""
Create a new image capture object. Adds to the image list, and shunt all others.
Create a new image capture object.
Args:
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp.
@ -275,37 +286,35 @@ class BaseCamera(metaclass=ABCMeta):
filename = "{}.{}".format(filename, fmt)
# Generate folder
base_folder = self.paths['image_tmp'] if temporary else self.paths['image']
base_folder = self.paths["temp"] if temporary else self.paths["default"]
folder = os.path.join(base_folder, folder)
# Generate file path
filepath = os.path.join(folder, filename)
# Create capture object
output = CaptureObject(
write_to_file=write_to_file,
temporary=temporary,
filepath=filepath)
output = CaptureObject(filepath=filepath)
# Insert a temporary tag if temporary
if temporary:
output.put_tags(['temporary'])
# Update capture list
shunt_captures(self.images)
self.images.append(output)
return output
def new_video(
self,
write_to_file: bool = True,
temporary: bool = False,
filename: str = None,
folder: str = "",
fmt: str = 'h264'):
self,
temporary: bool = False,
filename: str = None,
folder: str = "",
fmt: str = "h264",
):
"""
Create a new video capture object. Adds to the image list, and shunt all others.
Create a new video capture object.
Args:
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp.
@ -320,20 +329,19 @@ class BaseCamera(metaclass=ABCMeta):
filename = "{}.{}".format(filename, fmt)
# Generate folder
base_folder = self.paths['video_tmp'] if temporary else self.paths['video']
base_folder = self.paths["temp"] if temporary else self.paths["default"]
folder = os.path.join(base_folder, folder)
# Generate file path
filepath = os.path.join(folder, filename)
# Create capture object
output = CaptureObject(
write_to_file=write_to_file,
temporary=temporary,
filepath=filepath)
output = CaptureObject(filepath=filepath)
# Insert a temporary tag if temporary
if temporary:
output.put_tags(['temporary'])
# Update capture list
shunt_captures(self.videos)
self.videos.append(output)
return output
@ -345,7 +353,7 @@ class BaseCamera(metaclass=ABCMeta):
self.frames_iterator = self.frames()
logging.debug("Entering worker thread.")
self.state['stream_active'] = True
self.state["stream_active"] = True
for frame in self.frames_iterator:
self.frame = frame
@ -354,9 +362,13 @@ class BaseCamera(metaclass=ABCMeta):
# Handle timeout
if (
self.stream_timeout_enabled and # If using timeout
(time.time() - self.last_access > self.stream_timeout) and # And timeout time
not self.state['preview_active'] # And GPU preview is not active
self.stream_timeout_enabled
and ( # If using timeout
time.time() - self.last_access > self.stream_timeout
)
and not self.state[ # And timeout time
"preview_active"
] # And GPU preview is not active
):
self.frames_iterator.close()
break
@ -372,4 +384,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state
self.state['stream_active'] = False
self.state["stream_active"] = False

View file

@ -12,32 +12,11 @@ import atexit
from openflexure_microscope.camera import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
"""
Attributes:
BASE_CAPTURE_PATH (str): Base path to store all captures
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
"""
PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF']
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
# TODO: Move these methods to a camera utilities module?
def clear_tmp():
"""
Removes all files in the ``TEMP_CAPTURE_PATH`` directory
"""
global TEMP_CAPTURE_PATH
if os.path.isdir(TEMP_CAPTURE_PATH):
logging.info("Clearing {}...".format(TEMP_CAPTURE_PATH))
shutil.rmtree(TEMP_CAPTURE_PATH)
logging.debug("Cleared {}.".format(TEMP_CAPTURE_PATH))
def pull_usercomment_dict(filepath):
"""
@ -50,8 +29,8 @@ def pull_usercomment_dict(filepath):
except InvalidImageDataError:
logging.error("Invalid data at {}. Skipping.".format(filepath))
return None
if 'Exif' in exif_dict and 37510 in exif_dict['Exif']:
return yaml.load(exif_dict['Exif'][37510].decode())
if "Exif" in exif_dict and 37510 in exif_dict["Exif"]:
return yaml.load(exif_dict["Exif"][37510].decode())
else:
return None
@ -59,18 +38,20 @@ def pull_usercomment_dict(filepath):
def make_file_list(directory, formats):
files = []
for fmt in formats:
files.extend(glob.glob('{}/**/*.{}'.format(directory, fmt.lower()), recursive=True))
files.extend(
glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True)
)
logging.info("{} capture files found on disk".format(len(files)))
return files
def build_captures_from_exif():
global BASE_CAPTURE_PATH, EXIF_FORMATS
def build_captures_from_exif(capture_path):
global EXIF_FORMATS
logging.debug("Reloading captures from {}...".format(BASE_CAPTURE_PATH))
files = make_file_list(BASE_CAPTURE_PATH, EXIF_FORMATS)
logging.debug("Reloading captures from {}...".format(capture_path))
files = make_file_list(capture_path, EXIF_FORMATS)
captures = []
for f in files:
@ -98,50 +79,40 @@ def capture_from_exif(path, exif_dict):
"""
# Create a placeholder capture
capture = CaptureObject(
filepath=path
)
capture = CaptureObject(filepath=path)
# Build file path information
capture.split_file_path(capture.file)
# Populate capture parameters
capture.id = exif_dict['id']
capture.timestring = exif_dict['time']
capture.format = exif_dict['format']
capture.id = exif_dict["id"]
capture._metadata = exif_dict['custom']
capture.tags = exif_dict['tags']
capture.timestring = exif_dict["time"]
capture.format = exif_dict["format"]
capture._metadata = exif_dict["custom"]
capture.tags = exif_dict["tags"]
return capture
class CaptureObject(object):
"""
StreamObject used to store and process capture data, and metadata.
StreamObject used to store and process on-disk capture data, and metadata.
Serves to simplify modifying properties of on-disk capture data.
Attributes:
timestring (str): Timestring of capture creation time
temporary (bool): Mark the capture as temporary, to be deleted as the server closes,
or as resources are required
_metadata (dict): Dictionary of custom metadata to be included in metadata file
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
bytestream (:py:class:`io.BytesIO`): Byte bytestream that data will be written to
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
basename (str): Filename of the capture, without a file extension
format (str): Format of the capture data
Notes:
Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH.
"""
def __init__(
self,
write_to_file: bool = False,
temporary: bool = False,
filepath: str = '') -> None:
def __init__(self, filepath) -> None:
"""Create a new StreamObject, to manage capture data."""
# Store a nice ID
@ -149,14 +120,8 @@ class CaptureObject(object):
logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Keep on disk after close by default
self.temporary = temporary
# Create file name. Default to UUID
if not filepath:
self.file = self.build_file_path()
else:
self.file = filepath
self.file = filepath
self.split_file_path(self.file)
# Dictionary for storing custom metadata
@ -165,50 +130,11 @@ class CaptureObject(object):
# List for storing tags
self.tags = []
# Byte bytestream properties
self.bytestream = io.BytesIO()
# Set default write target
if not write_to_file:
self.stream = self.bytestream
else:
self.stream = self.file
# Log if created by context manager
self.context_manager = False
# Thumbnail (populated only for PIL captures)
self.thumb_bytes = None
def __enter__(self):
"""Create StreamObject in context, to auto-clean disk data."""
logging.debug(
"Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(
self.id))
self.temporary = True # Flag file to be removed on close.
self.context_manager = True # Used in metadata
logging.info("Rebuilding as a temporary capture...")
self.build_file_path()
return self
def __exit__(self, *args):
"""Exit StreamObject, and auto-clean disk data."""
logging.info("Cleaning up {}".format(self.id))
self.close()
def build_file_path(self):
"""
Construct a full file path, based on filename, folder, and file format.
Defaults to UUID.
"""
global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH
if self.temporary:
base_dir = TEMP_CAPTURE_PATH
else:
base_dir = BASE_CAPTURE_PATH
return os.path.join(base_dir, self.filename) # Full file name by joining given folder to given name
def open(self, mode):
return open(self.file, mode)
def split_file_path(self, filepath):
"""
@ -221,26 +147,14 @@ class CaptureObject(object):
self.filefolder, self.filename = os.path.split(filepath)
# Split the filename out from it's file extension
self.basename = os.path.splitext(self.filename)[0]
self.format = self.filename.split('.')[-1]
self.format = self.filename.split(".")[-1]
# Create folder and file
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
@property
def stream_exists(self, auto_rewind=True) -> bool:
"""Check if capture data BytesIO bytestream exists in memory."""
if auto_rewind:
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.bytestream.getvalue(): # If data bytestream contains data
self.bytestream.seek(0) # Rewind the data bytes for reading
return True
else:
self.bytestream.seek(0) # Rewind the data bytes for reading
return False
@property
def file_exists(self) -> bool:
def exists(self) -> bool:
"""Check if capture data file exists on disk."""
if os.path.isfile(self.file):
return True
@ -291,14 +205,14 @@ class CaptureObject(object):
"""
global EXIF_FORMATS
if self.format.upper() in EXIF_FORMATS and self.file_exists:
if self.format.upper() in EXIF_FORMATS and self.exists:
logging.debug("Writing exif data to capture file")
# Extract current Exif data
exif_dict = piexif.load(self.file)
# Serialize metadata
metadata_string = yaml.safe_dump(self.metadata)
# Insert metadata into exif_dict
exif_dict['Exif'][piexif.ExifIFD.UserComment] = metadata_string.encode()
exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode()
# Convert new exif dict to exif bytes
exif_bytes = piexif.dump(exif_dict)
# Insert exif into file
@ -310,28 +224,18 @@ class CaptureObject(object):
Create basic metadata dictionary from basic capture data,
and any added custom metadata and tags.
"""
d = {'id': self.id, 'filename': self.filename, 'time': self.timestring,
'format': self.format, 'tags': self.tags, 'custom': self._metadata}
d = {
"id": self.id,
"filename": self.filename,
"time": self.timestring,
"format": self.format,
"tags": self.tags,
"custom": self._metadata,
}
# Add custom metadata to dictionary
return d
@property
def yaml(self) -> str:
"""
Return a string containing a YAML-formatted representation of the capture matadata
"""
return yaml.dump(self.metadata, default_flow_style=False)
@property
def exists(self) -> bool:
"""
Check if either an in-memory byte stream or on-disk file of capture data exists.
If False, the capture data cannot be obtained.
"""
return self.stream_exists or self.file_exists
@property
def state(self) -> dict:
"""
@ -339,19 +243,13 @@ class CaptureObject(object):
"""
# Create basic state dictionary
d = {'path': self.file, 'temporary': self.temporary, 'metadata': self.metadata}
# Check bytestream
if self.stream_exists:
d['bytestream'] = True
else:
d['bytestream'] = False
d = {"path": self.file, "metadata": self.metadata}
# Combined availability of data
if self.exists:
d['available'] = True
d["available"] = True
else:
d['available'] = False
d["available"] = False
return d
@ -359,27 +257,17 @@ class CaptureObject(object):
def data(self) -> io.BytesIO:
"""
Return a byte string of the capture data.
If the capture data exists in-memory, this will be loaded. If the data only exists
on disk, this will automatically fall back to loading from disk.
"""
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.stream_exists: # If data bytestream contains data
if self.exists: # If data file exists
logging.info("Opening from file {}".format(self.file))
with open(self.file, "rb") as f:
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes
logging.debug("STREAM EXISTS")
data = io.BytesIO(self.bytestream.getbuffer())
else: # If data bytestream is empty
if self.file_exists: # If data file exists
logging.info("Opening from file {}".format(self.file))
with open(self.file, 'rb') as f:
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes
data = io.BytesIO(d.getbuffer())
else:
data = None
data = io.BytesIO(d.getbuffer())
else:
data = None
return data # Read and return bytes data
@ -410,41 +298,13 @@ class CaptureObject(object):
data = io.BytesIO(self.thumb_bytes.getbuffer())
return data
def load_file(self) -> bool:
"""Load data stored on disk to the in-memory bytestream."""
if self.file_exists: # If data file exists
with open(self.file, 'rb') as f:
self.bytestream = io.BytesIO(f.read()) # Load bytes from file
self.bytestream.seek(0) # Rewind data bytes again
return True
else:
return False
def save_file(self) -> bool:
"""Write the StreamObjects bytestream to a file."""
if self.stream_exists: # If there's a bytestream to save
with open(self.file, 'ab') as f: # Load file as bytes
logging.debug("Writing bytestream to file {}".format(self.file))
f.seek(0, 0) # Seek to the start of the file
f.write(self.binary) # Write data bytes to file
return True
else:
return False
def save(self) -> None:
"""Write stream to file, and save/update metadata file"""
# Try to save the file (only succeeds if an unsaved stream exists)
self.save_file()
# If a stream OR file exists, save the metadata file
if self.exists:
self.save_metadata()
def delete_stream(self):
"""Clear the BytesIO bytestream of the StreamObject."""
self.bytestream = io.BytesIO()
def delete_file(self) -> bool:
def delete(self) -> bool:
"""If the StreamObject has been saved, delete the file."""
if os.path.isfile(self.file):
@ -455,25 +315,5 @@ class CaptureObject(object):
else:
return False
def delete(self):
"""Entirely delete all capture data."""
logging.info("Deleting {}".format(self.id))
self.delete_stream()
self.delete_file()
def shunt(self):
"""Demote the StreamObject from being stored in memory."""
if not self.file_exists: # If file doesn't already exist
self.save() # Save bytestream to disk, if it exists
self.delete_stream() # Delete the bytestream from memory
def close(self):
"""Both clear the bytestream, and delete any associated on-disk data."""
logging.info("Closing {}".format(self.id))
self.delete_stream()
# Delete the file from disk if temporary
if self.temporary:
self.delete()
atexit.register(clear_tmp)
pass

View file

@ -25,7 +25,11 @@ class MockStreamer(BaseCamera):
BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.state.update({"stream_active": False, "record_active": False})
self.state.update({
"stream_active": False,
"record_active": False,
"board": None
})
# Update config properties
self.image_resolution = (1312, 976)
@ -71,12 +75,16 @@ class MockStreamer(BaseCamera):
Return config dictionary of the PiCameraStreamer.
"""
conf_dict = {
# Get config items from the base class
conf_dict = BaseCamera.read_config(self)
# Include device-specific config items
conf_dict.update({
"stream_resolution": self.stream_resolution,
"image_resolution": self.image_resolution,
"numpy_resolution": self.numpy_resolution,
"jpeg_quality": self.jpeg_quality,
}
})
return conf_dict
@ -90,24 +98,18 @@ class MockStreamer(BaseCamera):
Args:
config (dict): Dictionary of config parameters.
"""
paused_stream = False
logging.debug("PiCameraStreamer: Applying config:")
logging.debug("MockStreamer: Applying config:")
logging.debug(config)
with self.lock:
# Apply valid config params to Picamera object
# Apply valid config params to camera object
if not self.state["record_active"]: # If not recording a video
# PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting
if hasattr(self, key):
setattr(self, key, value)
# If stream was paused to update config, unpause
if paused_stream:
logging.info("Resuming stream.")
else:
raise Exception(
"Cannot update camera config while recording is active."
@ -135,7 +137,7 @@ class MockStreamer(BaseCamera):
Start a new video recording, writing to a output object.
Args:
output (CaptureObject/str): Output object to write data bytes to.
output: String or file-like object to write capture data to
fmt (str): Format of the capture.
quality (int): Video recording quality.
@ -167,7 +169,7 @@ class MockStreamer(BaseCamera):
Target object can be overridden for development purposes.
Args:
output (CaptureObject/str): Output object to write data bytes to.
output: String or file-like object to write capture data to
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
fmt (str): Format of the capture.
resize ((int, int)): Resize the captured image.
@ -175,7 +177,12 @@ class MockStreamer(BaseCamera):
"""
with self.lock:
logging.warning("Capture not implemented in mock camera")
if isinstance(output, str):
output = open(output, 'wb')
output.write(self.stream.getvalue())
output.close()
# HANDLE STREAM FRAMES

View file

@ -36,6 +36,7 @@ import picamera.array
from typing import Tuple
from .base import BaseCamera, CaptureObject
# Richard's fix gain
from .set_picamera_gain import set_analog_gain, set_digital_gain
@ -43,30 +44,35 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain
# MAIN CLASS
class PiCameraStreamer(BaseCamera):
"""Raspberry Pi camera implementation of PiCameraStreamer."""
picamera_settings_keys = [
'exposure_mode',
'analog_gain',
'digital_gain',
'shutter_speed',
'awb_gains',
'awb_mode',
'framerate',
'saturation',
'lens_shading_table'
"exposure_mode",
"analog_gain",
"digital_gain",
"shutter_speed",
"awb_gains",
"awb_mode",
"framerate",
"saturation",
"lens_shading_table",
]
def __init__(self):
# Run BaseCamera init
BaseCamera.__init__(self)
# Attach to Pi camera
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object
self.camera = (
picamera.PiCamera()
) #: :py:class:`picamera.PiCamera`: Picamera object
# Store state of PiCameraStreamer
self.state.update({
'stream_active': False,
'record_active': False,
'preview_active': False,
"stream_active": False,
"record_active": False,
"preview_active": False,
"board": f"picamera_{self.camera.revision}",
})
# Reset variable states
self.set_zoom(1.0)
@ -76,6 +82,9 @@ class PiCameraStreamer(BaseCamera):
self.numpy_resolution = (1312, 976)
self.jpeg_quality = 75
# Update board identifier
self.state.update({})
# Create an empty stream
self.stream = io.BytesIO()
@ -100,20 +109,25 @@ class PiCameraStreamer(BaseCamera):
Return config dictionary of the PiCameraStreamer.
"""
conf_dict = {
'stream_resolution': self.stream_resolution,
'image_resolution': self.image_resolution,
'numpy_resolution': self.numpy_resolution,
'jpeg_quality': self.jpeg_quality,
'picamera_settings': {},
}
# Get config items from the base class
conf_dict = BaseCamera.read_config(self)
# PiCamera parameters
# Include device-specific config items
conf_dict.update({
"stream_resolution": self.stream_resolution,
"image_resolution": self.image_resolution,
"numpy_resolution": self.numpy_resolution,
"jpeg_quality": self.jpeg_quality,
"picamera_settings": {},
})
# Include a subset of picamera properties
# TODO: Expand this subset so we have more metadata?
for key in PiCameraStreamer.picamera_settings_keys:
try:
value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value))
conf_dict['picamera_settings'][key] = value
conf_dict["picamera_settings"][key] = value
except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key))
@ -129,7 +143,6 @@ class PiCameraStreamer(BaseCamera):
Args:
config (dict): Dictionary of config parameters.
"""
# TODO: Include timing and batching logic when applying PiCamera settings
paused_stream = False
logging.debug("PiCameraStreamer: Applying config:")
@ -138,21 +151,23 @@ class PiCameraStreamer(BaseCamera):
with self.lock:
# Apply valid config params to Picamera object
if not self.state['record_active']: # If not recording a video
if not self.state["record_active"]: # If not recording a video
# Pause stream while changing settings
if self.state['stream_active']: # If stream is active
if self.state["stream_active"]: # If stream is active
logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done
# PiCamera parameters
if 'picamera_settings' in config: # If new settings are given
self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True)
if "picamera_settings" in config: # If new settings are given
self.apply_picamera_settings(
config["picamera_settings"], pause_for_effect=True
)
# PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting
if (key != 'picamera_settings') and hasattr(self, key):
if (key != "picamera_settings") and hasattr(self, key):
setattr(self, key, value)
# If stream was paused to update config, unpause
@ -162,67 +177,84 @@ class PiCameraStreamer(BaseCamera):
else:
raise Exception(
"Cannot update camera config while recording is active.")
"Cannot update camera config while recording is active."
)
def apply_picamera_settings(self, settings_dict: dict, pause_for_effect: bool=True):
def apply_picamera_settings(
self, settings_dict: dict, pause_for_effect: bool = True
):
# Set exposure mode
if 'exposure_mode' in settings_dict:
logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode']))
self.camera.exposure_mode = settings_dict['exposure_mode']
if "exposure_mode" in settings_dict:
logging.debug(
"Applying exposure_mode: {}".format(settings_dict["exposure_mode"])
)
self.camera.exposure_mode = settings_dict["exposure_mode"]
# Apply gains and let them settle
if 'analog_gain' in settings_dict:
logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain']))
set_analog_gain(self.camera, float(settings_dict['analog_gain']))
if 'digital_gain' in settings_dict:
logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain']))
set_digital_gain(self.camera, float(settings_dict['digital_gain']))
if "analog_gain" in settings_dict:
logging.debug(
"Applying analog_gain: {}".format(settings_dict["analog_gain"])
)
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
if "digital_gain" in settings_dict:
logging.debug(
"Applying digital_gain: {}".format(settings_dict["digital_gain"])
)
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
# Apply shutter speed
if 'shutter_speed' in settings_dict:
logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed']))
self.camera.shutter_speed = int(settings_dict['shutter_speed'])
if "shutter_speed" in settings_dict:
logging.debug(
"Applying shutter_speed: {}".format(settings_dict["shutter_speed"])
)
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
time.sleep(0.2) # Let gains settle
# Handle AWB in a half-smart way
if 'awb_gains' in settings_dict:
if "awb_gains" in settings_dict:
logging.debug("Applying awb_mode: off")
self.camera.awb_mode = 'off'
logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains']))
self.camera.awb_gains = settings_dict['awb_gains']
elif 'awb_mode' in settings_dict:
logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode']))
self.camera.awb_mode = settings_dict['awb_mode']
self.camera.awb_mode = "off"
logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"]))
self.camera.awb_gains = settings_dict["awb_gains"]
elif "awb_mode" in settings_dict:
logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"]))
self.camera.awb_mode = settings_dict["awb_mode"]
# Handle some properties that can be quickly applied
batched_keys = ['framerate', 'saturation']
batched_keys = ["framerate", "saturation"]
for key in batched_keys:
if (key in settings_dict) and hasattr(self.camera, key):
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
setattr(self.camera, key, settings_dict[key])
# Handle lens shading if camera supports it
if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'):
logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table']))
self.camera.lens_shading_table = settings_dict['lens_shading_table']
if ("lens_shading_table" in settings_dict) and hasattr(
self.camera, "lens_shading_table"
):
logging.debug(
"Applying lens_shading_table: {}".format(
settings_dict["lens_shading_table"]
)
)
self.camera.lens_shading_table = settings_dict["lens_shading_table"]
# Final optional pause to settle
if pause_for_effect:
time.sleep(0.2)
def set_zoom(self, zoom_value: float = 1.) -> None:
def set_zoom(self, zoom_value: float = 1.0) -> None:
"""
Change the camera zoom, handling re-centering and scaling.
"""
with self.lock:
self.state['zoom_value'] = float(zoom_value)
if self.state['zoom_value'] < 1:
self.state['zoom_value'] = 1
self.state["zoom_value"] = float(zoom_value)
if self.state["zoom_value"] < 1:
self.state["zoom_value"] = 1
# Richard's code for zooming !
fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
size = 1.0 / self.state['zoom_value']
size = 1.0 / self.state["zoom_value"]
# If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant
# when zooming out, if the FoV is not centred on (0.5, 0.5)
@ -239,9 +271,6 @@ class PiCameraStreamer(BaseCamera):
"""Start the on board GPU camera preview."""
logging.info("Starting the GPU preview")
# TODO: Commented out as I honestly can't remember why this was here. May need to put back?
# self.start_stream_recording()
try:
if not self.camera.preview:
logging.debug("Starting preview")
@ -252,28 +281,30 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window
if fullscreen:
self.camera.preview.fullscreen = fullscreen
self.state['preview_active'] = True
self.state["preview_active"] = True
except picamera.exc.PiCameraMMALError as e:
logging.error("Suppressed a MMALError in start_preview. Exception: {}".format(e))
logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
)
except picamera.exc.PiCameraValueError as e:
logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e))
logging.error(
"Suppressed a ValueError exception in start_preview. Exception: {}".format(
e
)
)
def stop_preview(self):
"""Stop the on board GPU camera preview."""
self.camera.stop_preview()
self.state['preview_active'] = False
self.state["preview_active"] = False
def start_recording(
self,
output,
fmt: str = 'h264',
quality: int = 15):
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
"""Start recording.
Start a new video recording, writing to a output object.
Args:
output (CaptureObject/str): Output object to write data bytes to.
output: String or file-like object to write capture data to
fmt (str): Format of the capture.
quality (int): Video recording quality.
@ -283,34 +314,29 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
# Start recording method only if a current recording is not running
if not self.state['record_active']:
# If output is a StreamObject
if isinstance(output, CaptureObject):
# Set target to capture stream
output_stream = output.stream
else:
output_stream = output
if not self.state["record_active"]:
# Start the camera video recording on port 2
logging.info("Recording to {}".format(output))
self.camera.start_recording(
output_stream,
output,
format=fmt,
splitter_port=2,
resize=self.stream_resolution,
quality=quality)
quality=quality,
)
# Update state dictionary
self.state['record_active'] = True
self.state["record_active"] = True
return output
else:
logging.error(
"Cannot start a new recording\
until the current recording has stopped.")
until the current recording has stopped."
)
return None
def stop_recording(self):
@ -322,12 +348,11 @@ class PiCameraStreamer(BaseCamera):
logging.info("Recording stopped")
# Update state dictionary
self.state['record_active'] = False
self.state["record_active"] = False
def stop_stream_recording(
self,
splitter_port: int = 1,
resolution: Tuple[int, int] = None) -> None:
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
) -> None:
"""
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
@ -346,16 +371,21 @@ class PiCameraStreamer(BaseCamera):
except picamera.exc.PiCameraNotRecording:
logging.info("Not recording on splitter_port {}".format(splitter_port))
else:
logging.info("Stopped MJPEG stream on port {1}. Switching to {0}.".format(resolution, splitter_port))
logging.info(
"Stopped MJPEG stream on port {1}. Switching to {0}.".format(
resolution, splitter_port
)
)
# Increase the resolution for taking an image
time.sleep(0.2) # Sprinkled a sleep to prevent camera getting confused by rapid commands
time.sleep(
0.2
) # Sprinkled a sleep to prevent camera getting confused by rapid commands
self.camera.resolution = resolution
def start_stream_recording(
self,
splitter_port: int = 1,
resolution: Tuple[int, int] = None) -> None:
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
) -> None:
"""
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
@ -366,45 +396,57 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
# If stream object was destroyed
if not hasattr(self, 'stream'):
if not hasattr(self, "stream"):
self.stream = io.BytesIO() # Create a stream object
# If no explicit resolution is passed
if not resolution:
resolution = self.stream_resolution # Default to video recording resolution
resolution = (
self.stream_resolution
) # Default to video recording resolution
# Reduce the resolution for video streaming
try:
self.camera._check_recording_stopped()
except picamera.exc.PiCameraRuntimeError:
logging.info("Error while changing resolution: Recording already running.")
logging.info(
"Error while changing resolution: Recording already running."
)
else:
self.camera.resolution = resolution
# If the stream should be active
if self.state['stream_active']:
if self.state["stream_active"]:
try:
# Start recording on stream port
self.camera.start_recording(
self.stream,
format='mjpeg',
format="mjpeg",
quality=self.jpeg_quality,
bitrate=-1, # RWB: disable bitrate control
bitrate=-1, # RWB: disable bitrate control
# (bitrate control makes JPEG size less good as a focus
# metric)
splitter_port=splitter_port)
splitter_port=splitter_port,
)
except picamera.exc.PiCameraAlreadyRecording:
logging.info("Error while starting preview: Recording already running.")
logging.info(
"Error while starting preview: Recording already running."
)
else:
logging.debug("Started MJPEG stream at {} on port {}".format(resolution, splitter_port))
logging.debug(
"Started MJPEG stream at {} on port {}".format(
resolution, splitter_port
)
)
def capture(
self,
output,
fmt: str = 'jpeg',
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = True):
self,
output,
fmt: str = "jpeg",
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = True,
):
"""
Capture a still image to a StreamObject.
@ -412,7 +454,7 @@ class PiCameraStreamer(BaseCamera):
Target object can be overridden for development purposes.
Args:
output (CaptureObject/str): Output object to write data bytes to.
output: String or file-like object to write capture data to
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
fmt (str): Format of the capture.
resize ((int, int)): Resize the captured image.
@ -420,13 +462,6 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
# If output is a StreamObject
if isinstance(output, CaptureObject):
# Set target to capture stream
output_stream = output.stream
else:
output_stream = output
logging.info("Capturing to {}".format(output))
# Set resolution and stop stream recording if necessary
@ -434,12 +469,13 @@ class PiCameraStreamer(BaseCamera):
self.stop_stream_recording()
self.camera.capture(
output_stream,
output,
format=fmt,
quality=100,
resize=resize,
bayer=(not use_video_port) and bayer,
use_video_port=use_video_port)
use_video_port=use_video_port,
)
# Set resolution and start stream recording if necessary
if not use_video_port:
@ -448,9 +484,8 @@ class PiCameraStreamer(BaseCamera):
return output
def yuv(
self,
use_video_port: bool = True,
resize: Tuple[int, int] = None) -> np.ndarray:
self, use_video_port: bool = True, resize: Tuple[int, int] = None
) -> np.ndarray:
"""Capture an uncompressed still YUV image to a Numpy array.
Args:
@ -477,10 +512,8 @@ class PiCameraStreamer(BaseCamera):
logging.info("Capturing to {}".format(output))
self.camera.capture(
output,
resize=size,
format='yuv',
use_video_port=use_video_port)
output, resize=size, format="yuv", use_video_port=use_video_port
)
if not use_video_port:
self.start_stream_recording()
@ -488,9 +521,8 @@ class PiCameraStreamer(BaseCamera):
return output.array
def array(
self,
use_video_port: bool = True,
resize: Tuple[int, int] = None) -> np.ndarray:
self, use_video_port: bool = True, resize: Tuple[int, int] = None
) -> np.ndarray:
"""Capture an uncompressed still RGB image to a Numpy array.
Args:
@ -517,10 +549,8 @@ class PiCameraStreamer(BaseCamera):
logging.info("Capturing to {}".format(output))
self.camera.capture(
output,
resize=size,
format='rgb',
use_video_port=use_video_port)
output, resize=size, format="rgb", use_video_port=use_video_port
)
# Resume stream
self.start_stream_recording()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,10 @@ import errno
import logging
import shutil
import copy
import numpy as np
from fractions import Fraction
from collections import abc
from .utilities import recursively_apply
"""
Attributes:
@ -18,7 +20,7 @@ Attributes:
# HANDLE THE DEFAULT CONFIGURATION FILE
HERE = os.path.abspath(os.path.dirname(__file__))
DEFAULT_CONFIG_PATH = os.path.join(HERE, 'microscope_settings.default.yaml')
DEFAULT_CONFIG_PATH = os.path.join(HERE, "microscope_settings.default.yaml")
USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")
@ -26,7 +28,7 @@ USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")
USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.yaml")
# Load the default config
with open(DEFAULT_CONFIG_PATH, 'r') as default_rc:
with open(DEFAULT_CONFIG_PATH, "r") as default_rc:
DEFAULT_CONFIG = default_rc.read()
# HANDLE CUSTOM YAML PARSING
@ -36,76 +38,50 @@ def construct_yaml_bool(self, node):
YAML otherwise caused grief for picamera properties where 'off' is a valid string value.
"""
override_bool_values = {
'yes': True,
'no': False,
'true': True,
'false': False,
'on': 'on',
'off': 'off',
"yes": True,
"no": False,
"true": True,
"false": False,
"on": "on",
"off": "off",
}
value = self.construct_scalar(node)
return override_bool_values[value.lower()]
yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool)
yaml.Loader.add_constructor(u"tag:yaml.org,2002:bool", construct_yaml_bool)
# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON
def convert_type_to_json_safe(v):
"""Make an individual attribute JSON-safe"""
if isinstance(v, Fraction):
return float(v)
elif isinstance(v, np.ndarray):
return v.tolist()
else:
return v
def recursively_apply(data, func):
"""
Recursively apply a function to a dictionary, list, array, or tuple
Args:
data: Input iterable data
func: Function to apply to all non-iterable values
"""
# If the object is a dictionary
if isinstance(data, abc.Mapping):
return {key: recursively_apply(val, func) for key, val in data.items()}
# If the object is iterable but NOT a dictionary or a string
elif (isinstance(data, abc.Iterable) and
not isinstance(data, abc.Mapping) and
not isinstance(data, str)):
return [recursively_apply(x, func) for x in data]
# if the object is neither a map nor iterable
else:
return func(data)
def settings_to_json(data: dict, clean_keys=True):
def settings_to_json(data: dict):
"""
Make a copy of an input dictionary that's safe for JSON return
Args:
data: Input dictionary
clean_keys: Modify any keys unsuitable for JSON return
"""
# Do not overwrite original data dictionary
d = copy.deepcopy(data)
# If we're cleaning up unsuitable keys
if clean_keys:
# Convert lens_shading_table to a bool
if 'picamera_settings' in d['camera_settings'] and 'lens_shading_table' in d['camera_settings']['picamera_settings']:
logging.debug("Bool-ifying lens_shading_table")
if d['camera_settings']['picamera_settings']['lens_shading_table'] is not None:
d['camera_settings']['picamera_settings']['lens_shading_table'] = True
return recursively_apply(d, convert_type_to_json_safe)
# HANDLE BASIC LOADING AND SAVING OF YAML FILES
def load_yaml_file(config_path) -> dict:
"""
Open a .yaml config file
@ -138,7 +114,7 @@ def save_yaml_file(config_path: str, config_dict: dict, safe: bool = False):
logging.info("Saving {}...".format(config_path))
logging.debug(config_dict)
with open(config_path, 'w') as outfile:
with open(config_path, "w") as outfile:
if not safe:
yaml.dump(config_dict, outfile)
else:
@ -173,7 +149,7 @@ def initialise_file(config_path, populate: str = ""):
create_file(config_path)
logging.info("Populating {}...".format(config_path))
with open(config_path, 'w') as outfile:
with open(config_path, "w") as outfile:
outfile.write(populate)
@ -186,12 +162,13 @@ class OpenflexureSettingsFile:
config_path (str): Path to the config YAML file (None falls back to default location)
expand (bool): Expand paths to valid auxillary config files.
"""
def __init__(self, config_path: str = None, expand: bool = True):
global DEFAULT_CONFIG, USER_CONFIG_FILE
self.expandable_keys = {
'camera_settings': None,
'stage_settings': None
"camera_settings": None,
"stage_settings": None,
} #: Dictionary of keys that can be passed as a file path string and expanded automatically
# Set arguments
@ -226,10 +203,10 @@ class OpenflexureSettingsFile:
save_config = self.contract_config(config)
else:
save_config = config
if backup:
if os.path.isfile(self.config_path):
shutil.copyfile(self.config_path, self.config_path+".bk")
shutil.copyfile(self.config_path, self.config_path + ".bk")
logging.debug("Saving settings dictionary to disk")
save_yaml_file(self.config_path, save_config)
@ -255,8 +232,7 @@ class OpenflexureSettingsFile:
# For each value in the raw loaded config
for key, value in config_dict.items():
# If it's a valid expandable parameter
if (key in self.expandable_keys and
type(value) is str):
if key in self.expandable_keys and type(value) is str:
logging.debug("Expanding {}".format(value))
@ -282,9 +258,11 @@ class OpenflexureSettingsFile:
# For each value in the expanded config
for key, value in config_dict.items():
# If it's a valid expandable parameter
if (key in self.expandable_keys and
self.expandable_keys[key] is not None and
type(value) is dict):
if (
key in self.expandable_keys
and self.expandable_keys[key] is not None
and type(value) is dict
):
logging.debug("Saving to {}".format(self.expandable_keys[key]))
# Create the file if it doesn't exist

View file

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

View file

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

View file

@ -90,9 +90,6 @@ class Microscope:
settings_full = self.read_config()
# TODO: Actually attach dummy hardware!
# Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists
logging.debug("Attaching camera...")
self.camera = (
camera
@ -217,7 +214,7 @@ class Microscope:
settings_full = self.settings_file.merge(settings_current)
if json_safe:
settings_full = settings_to_json(settings_full, clean_keys=True)
settings_full = settings_to_json(settings_full)
return settings_full

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import copy
import operator
from collections import abc
from functools import reduce
from contextlib import contextmanager
@ -16,7 +17,11 @@ def set_properties(obj, **kwargs):
try:
saved_properties[k] = getattr(obj, k)
except AttributeError:
print("Warning: could not get {} on {}. This property will not be restored!".format(k, obj))
print(
"Warning: could not get {} on {}. This property will not be restored!".format(
k, obj
)
)
for k, v in kwargs.items():
setattr(obj, k, v)
try:
@ -26,12 +31,14 @@ def set_properties(obj, **kwargs):
setattr(obj, k, v)
def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None, asint=True):
def axes_to_array(
coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True
):
"""Takes key-value pairs of a JSON value, and maps onto an array"""
# If no base array is given
if not base_array:
# Create an array of zeros
base_array = [0]*len(axis_keys)
base_array = [0] * len(axis_keys)
else:
# Create a copy of the passed base_array
base_array = copy.copy(base_array)
@ -39,7 +46,9 @@ def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=N
# Do the mapping
for axis, key in enumerate(axis_keys):
if key in coordinate_dictionary:
base_array[axis] = int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
base_array[axis] = (
int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
)
return base_array
@ -61,3 +70,26 @@ def entry_by_id(entry_id: str, object_list: list):
if o.id == entry_id:
found = o
return found
def recursively_apply(data, func):
"""
Recursively apply a function to a dictionary, list, array, or tuple
Args:
data: Input iterable data
func: Function to apply to all non-iterable values
"""
# If the object is a dictionary
if isinstance(data, abc.Mapping):
return {key: recursively_apply(val, func) for key, val in data.items()}
# If the object is iterable but NOT a dictionary or a string
elif (
isinstance(data, abc.Iterable)
and not isinstance(data, abc.Mapping)
and not isinstance(data, str)
):
return [recursively_apply(x, func) for x in data]
# if the object is neither a map nor iterable
else:
return func(data)

5
poetry.lock generated
View file

@ -249,7 +249,6 @@ version = "1.13.1b0"
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git"
url = "https://github.com/rwb27/picamera.git"
[[package]]
category = "main"
description = "Python Imaging Library (Fork)"
@ -498,8 +497,8 @@ python-versions = "*"
version = "1.11.2"
[metadata]
content-hash = "9c0dc80a1676c91381a79f07dc862af63a85314acd3cd850a6ca35fe01dfe33d"
python-versions = "^3.7"
content-hash = "2f6112291b3180745f71b9663a8c320291acfc8fa97a0b9ca84b1dbd566d2109"
python-versions = "^3.6"
[metadata.hashes]
alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"]

View file

@ -23,7 +23,7 @@ keywords = ['raspberry pi', 'arduino', 'microscope']
license = "GPL-3.0"
[tool.poetry.dependencies]
python = "^3.7"
python = "^3.6"
Flask = "^1.0"
pyyaml = "^3.13"
numpy = "1.16.4" # Exact version until we can guarantee a wheel

View file

@ -30,63 +30,61 @@ class APIconnection:
return r.json()
def set_overlay(self, message="", size=50):
json = {
"text": message,
"size": size
}
return self.post('/camera/overlay', json=json)
json = {"text": message, "size": size}
return self.post("/camera/overlay", json=json)
def get_overlay(self):
return self.get('/camera/overlay')
return self.get("/camera/overlay")
def get_config(self):
return self.get('/config')
return self.get("/config")
def set_config(self, config_dict):
return self.post('/config', json=config_dict)
return self.post("/config", json=config_dict)
def get_state(self):
return self.get('/state')
return self.get("/state")
def start_preview(self):
return self.post('/camera/preview/start')
return self.post("/camera/preview/start")
def stop_preview(self):
return self.post('/camera/preview/stop')
return self.post("/camera/preview/stop")
def move_by(self, x=0, y=0, z=0):
json = {
"x": x,
"y": y,
"z": z
}
return self.post('/stage/position', json=json)
json = {"x": x, "y": y, "z": z}
return self.post("/stage/position", json=json)
def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None):
json = {
"keep_on_disk": keep_on_disk,
"use_video_port": use_video_port
}
json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port}
if resize:
json['size'] = {'width': resize[0], 'height': resize[1]}
json["size"] = {"width": resize[0], "height": resize[1]}
return self.post('/camera/capture', json=json)
return self.post("/camera/capture", json=json)
def get_capture(self, capture_id):
uri_route = '/camera/capture/{}/download'.format(capture_id)
uri_route = "/camera/capture/{}/download".format(capture_id)
r = self.get(uri_route, json=False)
img = Image.open(BytesIO(r.content))
array = np.asarray(img, dtype=np.int32)
return array
def del_capture(self, capture_id):
uri_route = '/camera/capture/{}'.format(capture_id)
uri_route = "/camera/capture/{}".format(capture_id)
return self.delete(uri_route)
def capture(self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None):
p = self.new_capture(use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize)
capture_id = p['metadata']['id']
def capture(
self,
use_video_port=True,
keep_on_disk=False,
delete_after_use=True,
resize=None,
):
p = self.new_capture(
use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize
)
capture_id = p["metadata"]["id"]
img_array = self.get_capture(capture_id)
if delete_after_use:
@ -95,10 +93,8 @@ class APIconnection:
return img_array
def set_zoom(self, zoom_value=1.0):
json = {
"zoom_value": zoom_value,
}
return self.post('/camera/zoom', json=json)
json = {"zoom_value": zoom_value}
return self.post("/camera/zoom", json=json)
def get_zoom(self):
return self.get('/camera/zoom')
return self.get("/camera/zoom")

View file

@ -6,27 +6,23 @@ import unittest
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
class TestCapture(unittest.TestCase):
def test_capture_config(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
config = connection.get_config()
expected_keys = [
'image_resolution',
'stream_resolution',
'numpy_resolution',
]
expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"]
for key in expected_keys:
self.assertTrue(key in config)
def test_capture_videoport(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
resolution = connection.get_config()['stream_resolution']
resolution = connection.get_config()["stream_resolution"]
for resize in [None, (640, 480)]:
@ -34,16 +30,17 @@ class TestCapture(unittest.TestCase):
resolution = resize
capture_array = connection.capture(
use_video_port=True,
keep_on_disk=False,
use_video_port=True,
keep_on_disk=False,
delete_after_use=True,
resize=resize)
resize=resize,
)
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
def test_capture_full(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
resolution = connection.get_config()['image_resolution']
resolution = connection.get_config()["image_resolution"]
for resize in [None, (640, 480)]:
@ -51,23 +48,21 @@ class TestCapture(unittest.TestCase):
resolution = resize
capture_array = connection.capture(
use_video_port=False,
keep_on_disk=False,
use_video_port=False,
keep_on_disk=False,
delete_after_use=True,
resize=resize)
resize=resize,
)
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
class TestStage(unittest.TestCase):
def test_stage_config(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
config = connection.get_config()
expected_keys = [
'backlash',
]
expected_keys = ["backlash"]
for key in expected_keys:
self.assertTrue(key in config)
@ -76,14 +71,12 @@ class TestStage(unittest.TestCase):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
state = connection.get_state()
self.assertTrue('stage' in state)
expected_keys = [
'position',
]
self.assertTrue("stage" in state)
expected_keys = ["position"]
for key in expected_keys:
self.assertTrue(key in state['stage'])
self.assertTrue(key in state["stage"])
def test_stage_movement(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
@ -91,16 +84,16 @@ class TestStage(unittest.TestCase):
move_distance = 500
for axis in range(3):
for direction in [1, -1]:
pos_i_dict = connection.get_state()['stage']['position']
pos_i = [pos_i_dict['x'], pos_i_dict['y'], pos_i_dict['z']]
pos_i_dict = connection.get_state()["stage"]["position"]
pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]]
move = [0, 0, 0]
move[axis] = move_distance*direction
move[axis] = move_distance * direction
connection.move_by(*move)
pos_f_dict = connection.get_state()['stage']['position']
pos_f = [pos_f_dict['x'], pos_f_dict['y'], pos_f_dict['z']]
pos_f_dict = connection.get_state()["stage"]["position"]
pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]]
diff = np.subtract(pos_f, pos_i)
logging.debug("{} > {}".format(pos_i, pos_f))
@ -108,7 +101,7 @@ class TestStage(unittest.TestCase):
self.assertTrue(np.array_equal(diff, move))
if __name__ == '__main__':
if __name__ == "__main__":
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestCapture),

View file

@ -12,11 +12,11 @@ import unittest
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
class TestCaptureMethods(unittest.TestCase):
def test_still_capture(self):
"""Tests capturing still images to a BytesIO stream."""
global camera
@ -28,13 +28,9 @@ class TestCaptureMethods(unittest.TestCase):
camera.wait_for_camera()
# Capture to a context (auto-deletes files when done)
with camera.new_image(write_to_file=False) as output:
with camera.new_image() as output:
camera.capture(
output,
use_video_port=use_video_port,
resize=resize
)
camera.capture(output.file, use_video_port=use_video_port, resize=resize)
# Ensure file deletion fails and returns False
self.assertFalse(output.delete_file())
@ -43,14 +39,8 @@ class TestCaptureMethods(unittest.TestCase):
# BEFORE DELETE: Ensure StreamObject 'stream' has
# a valid BytesIO object and byte string
self.assertTrue(isinstance(
output.data,
io.IOBase
))
self.assertTrue(isinstance(
output.binary,
(bytes, bytearray)
))
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Save capture to file
output.save_file()
@ -65,10 +55,7 @@ class TestCaptureMethods(unittest.TestCase):
# AFTER DELETE: Ensure StreamObject 'stream' has
# a valid BytesIO object and byte string
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(
output.binary,
(bytes, bytearray)
))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Create a PIL image from stream
image = Image.open(output.data)
@ -100,9 +87,10 @@ class TestCaptureMethods(unittest.TestCase):
# Capture
output = camera.capture(
camera.new_image(write_to_file=True),
camera.new_image().file,
use_video_port=use_video_port,
resize=resize)
resize=resize,
)
# Check file got saved
self.assertTrue(os.path.isfile(output.file))
@ -122,7 +110,6 @@ class TestCaptureMethods(unittest.TestCase):
class TestUnencodedMethods(unittest.TestCase):
def test_yuv_array(self):
"""Tests capturing unencoded YUV data to a Numpy array."""
global camera
@ -136,9 +123,7 @@ class TestUnencodedMethods(unittest.TestCase):
camera.wait_for_camera()
# Capture RGB array
yuv = camera.yuv(
use_video_port=use_video_port,
resize=resize)
yuv = camera.yuv(use_video_port=use_video_port, resize=resize)
# Ensure capture output is a valid numpy array
self.assertTrue(isinstance(yuv, np.ndarray))
@ -168,9 +153,7 @@ class TestUnencodedMethods(unittest.TestCase):
camera.wait_for_camera()
# Capture RGB array
rgb = camera.array(
use_video_port=use_video_port,
resize=resize)
rgb = camera.array(use_video_port=use_video_port, resize=resize)
# Ensure capture output is a valid numpy array
self.assertTrue(isinstance(rgb, np.ndarray))
@ -189,41 +172,33 @@ class TestUnencodedMethods(unittest.TestCase):
class TestRecordMethods(unittest.TestCase):
def test_video_record(self):
"""Tests recording videos to BytesIO stream, and to file on disk."""
global camera
for write_to_file in [True, False, None]:
# Wait for camera
camera.wait_for_camera()
logging.debug("\nWRITE TO FILE: {}".format(write_to_file))
with camera.new_video() as output:
# Wait for camera
camera.wait_for_camera()
# Start recording
camera.start_recording(output)
with camera.new_video(
write_to_file=write_to_file
) as output:
# Record for 2 seconds
time.sleep(2)
# Stop recording
camera.stop_recording()
# Start recording
camera.start_recording(output)
# Check stream
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Record for 2 seconds
time.sleep(2)
# Stop recording
camera.stop_recording()
# Check file
statinfo = os.stat(output.file)
self.assertTrue(statinfo.st_size > 0)
# Check stream
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Check file
if write_to_file:
statinfo = os.stat(output.file)
self.assertTrue(statinfo.st_size > 0)
# Log path
temp_path = output.file
# Log path
temp_path = output.file
# Check file got deleted on __exit__
self.assertFalse(os.path.isfile(temp_path))
@ -278,7 +253,7 @@ class TestThreadStarting(unittest.TestCase):
self.assertIsInstance(camera.stream, io.IOBase)
if __name__ == '__main__':
if __name__ == "__main__":
with PiCameraStreamer() as camera:
suites = [

View file

@ -8,17 +8,17 @@ import atexit
import unittest
import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
class TestPluginMethods(unittest.TestCase):
def test_plugin_load(self):
plugin_arr = microscope.plugin.plugins
plugin_names = [plugin[0] for plugin in plugin_arr]
self.assertTrue('testing' in plugin_names)
self.assertTrue("testing" in plugin_names)
def test_camera_access(self):
identify = microscope.plugin.testing.identify()
@ -29,18 +29,16 @@ class TestPluginMethods(unittest.TestCase):
self.assertTrue(identify[1] is microscope.stage)
if __name__ == '__main__':
if __name__ == "__main__":
with Microscope() as microscope:
with Microscope() as microscope:
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin")
microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin")
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods),
]
suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
alltests = unittest.TestSuite(suites)
alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests)
result = unittest.TextTestRunner(verbosity=2).run(alltests)

View file

@ -12,7 +12,6 @@ logging.basicConfig(stream=sys.stderr, level=logging.INFO)
class TestMicroscope(unittest.TestCase):
def test_movement(self):
move_distance = 500
for axis in range(3):
@ -20,9 +19,11 @@ class TestMicroscope(unittest.TestCase):
pos_i = stage.position
logging.debug(pos_i)
logging.info("Moving axis {} by {}".format(axis, move_distance*direction))
logging.info(
"Moving axis {} by {}".format(axis, move_distance * direction)
)
move = [0, 0, 0]
move[axis] = move_distance*direction
move[axis] = move_distance * direction
stage.move_rel(move)
@ -33,12 +34,10 @@ class TestMicroscope(unittest.TestCase):
self.assertTrue(np.array_equal(diff, move))
if __name__ == '__main__':
if __name__ == "__main__":
with OpenFlexureStage("/dev/ttyUSB0") as stage:
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestMicroscope),
]
suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)]
alltests = unittest.TestSuite(suites)