Converted all configs to JSON
This commit is contained in:
parent
94a8774ceb
commit
b81cfaf9e7
13 changed files with 175 additions and 218 deletions
|
|
@ -11,7 +11,7 @@ Microscope RC file
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Microscope configuration is made persistent via a microscope runtime-config (RC) file. By default, this
|
Microscope configuration is made persistent via a microscope runtime-config (RC) file. By default, this
|
||||||
file exists at ``~/.openflexure/microscope_settings.yaml``, and contains basic parameters to set up the microscope.
|
file exists at ``~/.openflexure/microscope_settings.json``, and contains basic parameters to set up the microscope.
|
||||||
|
|
||||||
Additionally, by default, configurations for specific pieces of hardware (i.e. the stage and camera) are located
|
Additionally, by default, configurations for specific pieces of hardware (i.e. the stage and camera) are located
|
||||||
in separate "auxillary" config files, and are linked together by adding the config file's path to your main
|
in separate "auxillary" config files, and are linked together by adding the config file's path to your main
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,18 @@ Generally, for adding anything other than very simple functionality, plugins sho
|
||||||
|
|
||||||
The main restriction is that the plugin package must be importable using an absolute import from within the Python environment being used to load your microscope.
|
The main restriction is that the plugin package must be importable using an absolute import from within the Python environment being used to load your microscope.
|
||||||
|
|
||||||
Loading plugins with microscope_settings.yaml
|
Loading plugins with microscope_settings.json
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
Both types of plugin are loaded by specifying the plugin class in your :ref:`MicroscopeRC`. In the case of a single-file plugin, specify the path to the plugin file, followed by the name of your :py:class:`openflexure_microscope.plugins.MicroscopePlugin` child class, separated by a colon. For packaged plugins, specify the absolute module name in place of the path.
|
Both types of plugin are loaded by specifying the plugin class in your :ref:`MicroscopeRC`. In the case of a single-file plugin, specify the path to the plugin file, followed by the name of your :py:class:`openflexure_microscope.plugins.MicroscopePlugin` child class, separated by a colon. For packaged plugins, specify the absolute module name in place of the path.
|
||||||
|
|
||||||
For example, the plugins section of your microscope_settings.yaml file may look like:
|
For example, the plugins section of your microscope_settings.json file may look like:
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: json
|
||||||
|
|
||||||
...
|
...
|
||||||
plugins:
|
"plugins": [
|
||||||
- openflexure_microscope.plugins.default:Plugin
|
"openflexure_microscope.plugins.default:Plugin",
|
||||||
- ~/my_plugins/my_plugin_file.py:MyPluginClass
|
"~/my_plugins/my_plugin_file.py:MyPluginClass",
|
||||||
- my_openflexure_plugins.microscope.mypluginpackage:MyPluginClass
|
"my_openflexure_plugins.microscope.mypluginpackage:MyPluginClass"
|
||||||
|
]
|
||||||
...
|
...
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from openflexure_microscope.api.utilities import list_routes
|
||||||
from openflexure_microscope import Microscope
|
from openflexure_microscope import Microscope
|
||||||
|
|
||||||
from openflexure_microscope.camera.capture import build_captures_from_exif
|
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||||
from openflexure_microscope.config import USER_CONFIG_DIR
|
from openflexure_microscope.config import settings_file_path, JSONEncoder
|
||||||
from openflexure_microscope.api.v1 import blueprints
|
from openflexure_microscope.api.v1 import blueprints
|
||||||
|
|
||||||
# Import device modules
|
# Import device modules
|
||||||
|
|
@ -38,7 +38,7 @@ from openflexure_microscope.stage.mock import MockStage
|
||||||
# Handle logging
|
# Handle logging
|
||||||
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||||
|
|
||||||
DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, "openflexure_microscope.log")
|
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
|
||||||
|
|
||||||
if (__name__ == "__main__") or (not is_gunicorn):
|
if (__name__ == "__main__") or (not is_gunicorn):
|
||||||
# If imported, but not by gunicorn
|
# If imported, but not by gunicorn
|
||||||
|
|
@ -109,6 +109,10 @@ def uri(suffix, api_version, base=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.url_map.strict_slashes = False
|
app.url_map.strict_slashes = False
|
||||||
|
|
||||||
|
# Use custom JSON encoder
|
||||||
|
app.json_encoder = JSONEncoder
|
||||||
|
|
||||||
|
# Enable CORS everywhere
|
||||||
CORS(app, resources=r"*")
|
CORS(app, resources=r"*")
|
||||||
|
|
||||||
# Make errors more API friendly
|
# Make errors more API friendly
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,6 @@ 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
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
blueprint.add_url_rule(
|
|
||||||
"/capture/<capture_id>/metadata",
|
|
||||||
view_func=capture.MetadataRedirectAPI.as_view(
|
|
||||||
"metadata_download_redirect", microscope=microscope_obj
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tag routes
|
# Tag routes
|
||||||
blueprint.add_url_rule(
|
blueprint.add_url_rule(
|
||||||
"/capture/<capture_id>/tags",
|
"/capture/<capture_id>/tags",
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ class CaptureAPI(MicroscopeView):
|
||||||
|
|
||||||
def put(self, capture_id):
|
def put(self, capture_id):
|
||||||
"""
|
"""
|
||||||
Add arbitrary metadata to the capture (stored in an accompanying capture `.yaml` file)
|
Add arbitrary metadata to the capture
|
||||||
|
|
||||||
.. :quickref: Capture; Update capture metadata
|
.. :quickref: Capture; Update capture metadata
|
||||||
|
|
||||||
|
|
@ -351,76 +351,6 @@ class DownloadAPI(MicroscopeView):
|
||||||
return send_file(img, mimetype="image/jpeg")
|
return send_file(img, mimetype="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
class MetadataRedirectAPI(MicroscopeView):
|
|
||||||
def get(self, capture_id):
|
|
||||||
"""
|
|
||||||
Return metadatadata for a capture.
|
|
||||||
|
|
||||||
Return capture metadata as a YAML file with the requested filename.
|
|
||||||
I.e., `/(capture_id)/download/bar.yaml` will download the file as
|
|
||||||
`bar.yaml`, regardless of the capture's initially set filename.
|
|
||||||
|
|
||||||
Route automatically redirects to download the capture metadata under it's currently set filename.
|
|
||||||
I.e., `/(capture_id)/download` will
|
|
||||||
redirect to `/(capture_id)/download/(filename)`.
|
|
||||||
|
|
||||||
.. :quickref: Capture; Download capture metadata
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/metadata/2018-11-20_16-04-17.yaml HTTP/1.1
|
|
||||||
Accept: text/yaml
|
|
||||||
|
|
||||||
:>header Accept: text/yaml
|
|
||||||
:query as_attachment: return the image as an attachment download e.g. ?as_attachment=true
|
|
||||||
|
|
||||||
:>header Content-Type: text/yaml
|
|
||||||
:status 200: capture data found
|
|
||||||
:status 404: no capture found with that id
|
|
||||||
|
|
||||||
"""
|
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataAPI(MicroscopeView):
|
|
||||||
def get(self, capture_id, filename):
|
|
||||||
|
|
||||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Download the metadata using the requested filename
|
|
||||||
data = capture_obj.yaml
|
|
||||||
|
|
||||||
return Response(data, mimetype="text/yaml")
|
|
||||||
|
|
||||||
|
|
||||||
class TagsAPI(MicroscopeView):
|
class TagsAPI(MicroscopeView):
|
||||||
def get(self, capture_id):
|
def get(self, capture_id):
|
||||||
"""
|
"""
|
||||||
|
|
@ -433,11 +363,11 @@ class TagsAPI(MicroscopeView):
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||||
Accept: text/yaml
|
Accept: text/json
|
||||||
|
|
||||||
:>header Accept: text/yaml
|
:>header Accept: text/json
|
||||||
|
|
||||||
:>header Content-Type: text/yaml
|
:>header Content-Type: text/json
|
||||||
:status 200: capture data found
|
:status 200: capture data found
|
||||||
:status 404: no capture found with that id
|
:status 404: no capture found with that id
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
return {"paths": self.paths}
|
return {"paths": self.paths}
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
"""(Optional) Save any settings to disk that need to be stored"""
|
||||||
|
return
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Create camera on context enter."""
|
"""Create camera on context enter."""
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import shutil
|
||||||
import glob
|
import glob
|
||||||
import datetime
|
import datetime
|
||||||
import yaml
|
import yaml
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import atexit
|
import atexit
|
||||||
|
|
||||||
from openflexure_microscope.camera import piexif
|
from openflexure_microscope.camera import piexif
|
||||||
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
|
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
|
||||||
|
from openflexure_microscope.config import JSONEncoder
|
||||||
|
|
||||||
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
|
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
|
||||||
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
|
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
|
||||||
|
|
@ -30,7 +31,12 @@ def pull_usercomment_dict(filepath):
|
||||||
logging.warning("Invalid data at {}. Skipping.".format(filepath))
|
logging.warning("Invalid data at {}. Skipping.".format(filepath))
|
||||||
return None
|
return None
|
||||||
if "Exif" in exif_dict and 37510 in exif_dict["Exif"]:
|
if "Exif" in exif_dict and 37510 in exif_dict["Exif"]:
|
||||||
return yaml.load(exif_dict["Exif"][37510].decode())
|
try:
|
||||||
|
return json.loads(exif_dict["Exif"][37510].decode())
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
# TODO: Remove YAML support in a later version
|
||||||
|
logging.warning(f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON.")
|
||||||
|
return yaml.load(exif_dict["Exif"][37510].decode())
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -210,7 +216,7 @@ class CaptureObject(object):
|
||||||
# Extract current Exif data
|
# Extract current Exif data
|
||||||
exif_dict = piexif.load(self.file)
|
exif_dict = piexif.load(self.file)
|
||||||
# Serialize metadata
|
# Serialize metadata
|
||||||
metadata_string = yaml.safe_dump(self.metadata)
|
metadata_string = json.dumps(self.metadata, cls=JSONEncoder)
|
||||||
# Insert metadata into exif_dict
|
# Insert metadata into exif_dict
|
||||||
exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode()
|
exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode()
|
||||||
# Convert new exif dict to exif bytes
|
# Convert new exif dict to exif bytes
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from __future__ import division
|
||||||
import io
|
import io
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Pi camera
|
# Pi camera
|
||||||
|
|
@ -40,6 +41,7 @@ from .base import BaseCamera, CaptureObject
|
||||||
# Richard's fix gain
|
# Richard's fix gain
|
||||||
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||||
|
|
||||||
|
from openflexure_microscope.config import settings_file_path
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class PiCameraStreamer(BaseCamera):
|
class PiCameraStreamer(BaseCamera):
|
||||||
|
|
@ -54,10 +56,20 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"awb_mode",
|
"awb_mode",
|
||||||
"framerate",
|
"framerate",
|
||||||
"saturation",
|
"saturation",
|
||||||
"lens_shading_table",
|
"iso",
|
||||||
|
"brightness",
|
||||||
|
"contrast",
|
||||||
|
"crop",
|
||||||
|
"drc_strength",
|
||||||
|
"exposure_compensation",
|
||||||
|
"image_effect",
|
||||||
|
"meter_mode",
|
||||||
|
"sharpness"
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
global USER_CONFIG_DIR
|
||||||
|
|
||||||
# Run BaseCamera init
|
# Run BaseCamera init
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
# Attach to Pi camera
|
# Attach to Pi camera
|
||||||
|
|
@ -78,11 +90,13 @@ class PiCameraStreamer(BaseCamera):
|
||||||
# Reset variable states
|
# Reset variable states
|
||||||
self.set_zoom(1.0)
|
self.set_zoom(1.0)
|
||||||
|
|
||||||
# Update config properties
|
# Set default settings
|
||||||
self.image_resolution = tuple(self.camera.MAX_RESOLUTION)
|
self.image_resolution = tuple(self.camera.MAX_RESOLUTION) #: tuple: Resolution for image captures
|
||||||
self.stream_resolution = (832, 624)
|
self.stream_resolution = (832, 624) #: tuple: Resolution for stream and video captures
|
||||||
self.numpy_resolution = (1312, 976)
|
self.numpy_resolution = (1312, 976) #: tuple: Resolution for numpy array captures
|
||||||
self.jpeg_quality = 75
|
self.jpeg_quality = 75 #: int: JPEG quality
|
||||||
|
# Set default lens shading table path
|
||||||
|
self.picamera_lst_path = settings_file_path("picamera_lst.npy") #: str: Path of .npy lens shading table file
|
||||||
|
|
||||||
# Update board identifier
|
# Update board identifier
|
||||||
self.state.update({})
|
self.state.update({})
|
||||||
|
|
@ -121,12 +135,12 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"image_resolution": self.image_resolution,
|
"image_resolution": self.image_resolution,
|
||||||
"numpy_resolution": self.numpy_resolution,
|
"numpy_resolution": self.numpy_resolution,
|
||||||
"jpeg_quality": self.jpeg_quality,
|
"jpeg_quality": self.jpeg_quality,
|
||||||
|
"picamera_lst_path": self.picamera_lst_path if os.path.isfile(self.picamera_lst_path) else None,
|
||||||
"picamera_settings": {},
|
"picamera_settings": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include a subset of picamera properties
|
# Include a subset of picamera properties. Excludes lens shading table
|
||||||
# TODO: Expand this subset so we have more metadata?
|
|
||||||
for key in PiCameraStreamer.picamera_settings_keys:
|
for key in PiCameraStreamer.picamera_settings_keys:
|
||||||
try:
|
try:
|
||||||
value = getattr(self.camera, key)
|
value = getattr(self.camera, key)
|
||||||
|
|
@ -137,6 +151,11 @@ class PiCameraStreamer(BaseCamera):
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
"""Save lens-shading table to disk"""
|
||||||
|
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
|
||||||
|
self.save_lens_shading_table()
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_config(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Write a config dictionary to the PiCameraStreamer config.
|
Write a config dictionary to the PiCameraStreamer config.
|
||||||
|
|
@ -174,6 +193,17 @@ class PiCameraStreamer(BaseCamera):
|
||||||
if (key != "picamera_settings") and hasattr(self, key):
|
if (key != "picamera_settings") and hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
# Handle lens shading if camera supports it
|
||||||
|
if ("picamera_lst_path" in config) and hasattr(
|
||||||
|
self.camera, "lens_shading_table"
|
||||||
|
):
|
||||||
|
logging.debug(
|
||||||
|
"Applying lens_shading_table from file: {}".format(
|
||||||
|
config["picamera_lst_path"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.apply_lens_shading_table(config["picamera_lst_path"])
|
||||||
|
|
||||||
# If stream was paused to update config, unpause
|
# If stream was paused to update config, unpause
|
||||||
if paused_stream:
|
if paused_stream:
|
||||||
logging.info("Resuming stream.")
|
logging.info("Resuming stream.")
|
||||||
|
|
@ -232,21 +262,40 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
||||||
setattr(self.camera, key, settings_dict[key])
|
setattr(self.camera, key, settings_dict[key])
|
||||||
|
|
||||||
# Handle lens shading if camera supports it
|
|
||||||
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
|
# Final optional pause to settle
|
||||||
if pause_for_effect:
|
if pause_for_effect:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
def read_lens_shading_table(self):
|
||||||
|
"""
|
||||||
|
Read the current lens shading table as a numpy array, if it exists. Return None otherwise.
|
||||||
|
"""
|
||||||
|
if hasattr(self.camera, 'lens_shading_table'):
|
||||||
|
return self.camera.lens_shading_table
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_lens_shading_table(self):
|
||||||
|
"""
|
||||||
|
Save the current lens shading table to an .npy file, if it exists.
|
||||||
|
"""
|
||||||
|
logging.debug(self.read_lens_shading_table())
|
||||||
|
if self.read_lens_shading_table() is not None:
|
||||||
|
np.save(self.picamera_lst_path, self.read_lens_shading_table())
|
||||||
|
else:
|
||||||
|
logging.warning("Unable to save a nonexistant lens shading table")
|
||||||
|
|
||||||
|
def apply_lens_shading_table(self, lst_array_or_path):
|
||||||
|
"""
|
||||||
|
Apply a lens shading table from an .npy file, or numpy array.
|
||||||
|
"""
|
||||||
|
if isinstance(lst_array_or_path, np.ndarray):
|
||||||
|
self.camera.lens_shading_table = lst_array_or_path
|
||||||
|
elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path):
|
||||||
|
self.camera.lens_shading_table = np.load(lst_array_or_path)
|
||||||
|
else:
|
||||||
|
logging.error("Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping.")
|
||||||
|
|
||||||
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
||||||
"""
|
"""
|
||||||
Change the camera zoom, handling re-centering and scaling.
|
Change the camera zoom, handling re-centering and scaling.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import yaml
|
import json
|
||||||
import os
|
import os
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -13,103 +13,57 @@ from .utilities import recursively_apply
|
||||||
Attributes:
|
Attributes:
|
||||||
USER_CONFIG_DIR (str): Default path of the user-config directory, containing runtime-config and
|
USER_CONFIG_DIR (str): Default path of the user-config directory, containing runtime-config and
|
||||||
calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``.
|
calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``.
|
||||||
USER_CONFIG_FILE (str): Default path of the user microscope_settings.yaml runtime-config file.
|
USER_CONFIG_FILE (str): Default path of the user microscope_settings.json runtime-config file.
|
||||||
Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")``
|
Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.json")``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# HANDLE THE DEFAULT CONFIGURATION FILE
|
# HANDLE THE DEFAULT CONFIGURATION FILE
|
||||||
|
|
||||||
HERE = os.path.abspath(os.path.dirname(__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.json")
|
||||||
|
|
||||||
USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
|
USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
|
||||||
USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")
|
USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.json")
|
||||||
|
|
||||||
USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.yaml")
|
USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.json")
|
||||||
|
|
||||||
# Load the default config
|
# 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()
|
DEFAULT_CONFIG = default_rc.read()
|
||||||
|
|
||||||
# HANDLE CUSTOM YAML PARSING
|
|
||||||
def construct_yaml_bool(self, node):
|
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_file(config_path) -> dict:
|
||||||
"""
|
"""
|
||||||
Override PyYAML constructors handling of bools.
|
Open a .json config file
|
||||||
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",
|
|
||||||
}
|
|
||||||
value = self.construct_scalar(node)
|
|
||||||
return override_bool_values[value.lower()]
|
|
||||||
|
|
||||||
|
|
||||||
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.integer):
|
|
||||||
return int(v)
|
|
||||||
elif isinstance(v, np.ndarray):
|
|
||||||
return v.tolist()
|
|
||||||
else:
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def settings_to_json(data: dict):
|
|
||||||
"""
|
|
||||||
Make a copy of an input dictionary that's safe for JSON return
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Input dictionary
|
config_path (str): Path to the config JSON file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
|
||||||
"""
|
|
||||||
|
|
||||||
# Do not overwrite original data dictionary
|
|
||||||
d = copy.deepcopy(data)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
|
|
||||||
"""
|
"""
|
||||||
config_path = os.path.expanduser(config_path)
|
config_path = os.path.expanduser(config_path)
|
||||||
|
|
||||||
logging.info("Loading {}...".format(config_path))
|
logging.info("Loading {}...".format(config_path))
|
||||||
|
|
||||||
with open(config_path) as config_file:
|
with open(config_path) as config_file:
|
||||||
config_data = yaml.load(config_file)
|
try:
|
||||||
|
config_data = json.load(config_file)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
logging.error(e)
|
||||||
|
config_data = {}
|
||||||
|
|
||||||
# Return loaded config dictionary
|
# Return loaded config dictionary
|
||||||
return config_data
|
return config_data
|
||||||
|
|
||||||
|
|
||||||
def save_yaml_file(config_path: str, config_dict: dict, safe: bool = False):
|
def save_json_file(config_path: str, config_dict: dict):
|
||||||
"""
|
"""
|
||||||
Save a .yaml config file
|
Save a .json config file
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_dict (dict): Dictionary of config data to save.
|
config_dict (dict): Dictionary of config data to save.
|
||||||
config_path (str): Path to the config YAML file.
|
config_path (str): Path to the config JSON file.
|
||||||
safe (bool): Whether to use PyYAML safe_dump instead of dump
|
|
||||||
"""
|
"""
|
||||||
config_path = os.path.expanduser(config_path)
|
config_path = os.path.expanduser(config_path)
|
||||||
|
|
||||||
|
|
@ -117,10 +71,7 @@ def save_yaml_file(config_path: str, config_dict: dict, safe: bool = False):
|
||||||
logging.debug(config_dict)
|
logging.debug(config_dict)
|
||||||
|
|
||||||
with open(config_path, "w") as outfile:
|
with open(config_path, "w") as outfile:
|
||||||
if not safe:
|
json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True)
|
||||||
yaml.dump(config_dict, outfile)
|
|
||||||
else:
|
|
||||||
yaml.safe_dump(config_dict, outfile)
|
|
||||||
|
|
||||||
|
|
||||||
def create_file(config_path):
|
def create_file(config_path):
|
||||||
|
|
@ -132,7 +83,7 @@ def create_file(config_path):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def initialise_file(config_path, populate: str = ""):
|
def initialise_file(config_path, populate: str = "{}\n"):
|
||||||
"""
|
"""
|
||||||
Check if a file exists, and if not, create it
|
Check if a file exists, and if not, create it
|
||||||
and optionally populate it with content
|
and optionally populate it with content
|
||||||
|
|
@ -155,13 +106,35 @@ def initialise_file(config_path, populate: str = ""):
|
||||||
outfile.write(populate)
|
outfile.write(populate)
|
||||||
|
|
||||||
|
|
||||||
|
def settings_file_path(filename:str):
|
||||||
|
"""Generate a full file path for a filename to be stored in user settings"""
|
||||||
|
global USER_CONFIG_DIR
|
||||||
|
return os.path.join(USER_CONFIG_DIR, filename)
|
||||||
|
|
||||||
|
class JSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o, markers=None):
|
||||||
|
# PiCamera fractions
|
||||||
|
if isinstance(o, Fraction):
|
||||||
|
return float(o)
|
||||||
|
# Numpy integers
|
||||||
|
elif isinstance(o, np.integer):
|
||||||
|
return int(o)
|
||||||
|
# Numpy arrays
|
||||||
|
elif isinstance(o, np.ndarray):
|
||||||
|
return o.tolist()
|
||||||
|
else:
|
||||||
|
# call base class implementation which takes care of
|
||||||
|
# raising exceptions for unsupported types
|
||||||
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
# MAIN CONFIG CLASS
|
# MAIN CONFIG CLASS
|
||||||
|
|
||||||
class OpenflexureSettingsFile:
|
class OpenflexureSettingsFile:
|
||||||
"""
|
"""
|
||||||
An object to handle expansion, conversion, and saving of the microscope configuration.
|
An object to handle expansion, conversion, and saving of the microscope configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path (str): Path to the config YAML file (None falls back to default location)
|
config_path (str): Path to the config JSON file (None falls back to default location)
|
||||||
expand (bool): Expand paths to valid auxillary config files.
|
expand (bool): Expand paths to valid auxillary config files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -185,7 +158,7 @@ class OpenflexureSettingsFile:
|
||||||
Loads config from a file on-disk, and expands auxillary config files if available.
|
Loads config from a file on-disk, and expands auxillary config files if available.
|
||||||
"""
|
"""
|
||||||
# Unexpanded config dictionary (used at load/save time)
|
# Unexpanded config dictionary (used at load/save time)
|
||||||
loaded_config = load_yaml_file(self.config_path)
|
loaded_config = load_json_file(self.config_path)
|
||||||
|
|
||||||
# If the loaded config is in contracted format
|
# If the loaded config is in contracted format
|
||||||
if self.expand:
|
if self.expand:
|
||||||
|
|
@ -211,7 +184,7 @@ class OpenflexureSettingsFile:
|
||||||
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")
|
logging.debug("Saving settings dictionary to disk")
|
||||||
save_yaml_file(self.config_path, save_config)
|
save_json_file(self.config_path, save_config)
|
||||||
|
|
||||||
def merge(self, config: dict, backup: bool = True):
|
def merge(self, config: dict, backup: bool = True):
|
||||||
logging.debug("Merging settings with file on disk")
|
logging.debug("Merging settings with file on disk")
|
||||||
|
|
@ -243,7 +216,7 @@ class OpenflexureSettingsFile:
|
||||||
# Create the expansion file if it doesn't yet exist
|
# Create the expansion file if it doesn't yet exist
|
||||||
initialise_file(value)
|
initialise_file(value)
|
||||||
# Load the expansion file into _config
|
# Load the expansion file into _config
|
||||||
return_config[key] = load_yaml_file(value) or {}
|
return_config[key] = load_json_file(value) or {}
|
||||||
else:
|
else:
|
||||||
return_config[key] = value
|
return_config[key] = value
|
||||||
|
|
||||||
|
|
@ -270,7 +243,7 @@ class OpenflexureSettingsFile:
|
||||||
# Create the file if it doesn't exist
|
# Create the file if it doesn't exist
|
||||||
initialise_file(self.expandable_keys[key])
|
initialise_file(self.expandable_keys[key])
|
||||||
# Save the expanded config dictionary to the file
|
# Save the expanded config dictionary to the file
|
||||||
save_yaml_file(self.expandable_keys[key], value)
|
save_json_file(self.expandable_keys[key], value)
|
||||||
# Replace the expanded dictionary with a file path
|
# Replace the expanded dictionary with a file path
|
||||||
return_config[key] = self.expandable_keys[key]
|
return_config[key] = self.expandable_keys[key]
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from openflexure_microscope.camera.mock import MockStreamer
|
||||||
from openflexure_microscope.plugins import PluginMount
|
from openflexure_microscope.plugins import PluginMount
|
||||||
from openflexure_microscope.task import TaskOrchestrator
|
from openflexure_microscope.task import TaskOrchestrator
|
||||||
from openflexure_microscope.common.lock import CompositeLock
|
from openflexure_microscope.common.lock import CompositeLock
|
||||||
from openflexure_microscope.config import OpenflexureSettingsFile, settings_to_json
|
from openflexure_microscope.config import OpenflexureSettingsFile
|
||||||
|
|
||||||
|
|
||||||
class Microscope:
|
class Microscope:
|
||||||
|
|
@ -225,8 +225,8 @@ class Microscope:
|
||||||
|
|
||||||
settings_full = self.settings_file.merge(settings_current)
|
settings_full = self.settings_file.merge(settings_current)
|
||||||
|
|
||||||
if json_safe:
|
#if json_safe:
|
||||||
settings_full = settings_to_json(settings_full)
|
# settings_full = settings_to_json(settings_full)
|
||||||
|
|
||||||
return settings_full
|
return settings_full
|
||||||
|
|
||||||
|
|
@ -241,6 +241,10 @@ class Microscope:
|
||||||
"openflexure_microscope"
|
"openflexure_microscope"
|
||||||
).version
|
).version
|
||||||
# Save config to file
|
# Save config to file
|
||||||
|
if self.camera:
|
||||||
|
self.camera.save_config()
|
||||||
|
if self.stage:
|
||||||
|
self.stage.save_config()
|
||||||
self.settings_file.save(current_config, backup=True)
|
self.settings_file.save(current_config, backup=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
14
openflexure_microscope/microscope_settings.default.json
Normal file
14
openflexure_microscope/microscope_settings.default.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"fov": [
|
||||||
|
4100,
|
||||||
|
3146
|
||||||
|
],
|
||||||
|
"jpeg_quality": 75,
|
||||||
|
"camera_settings": "~/.openflexure/camera_settings.json",
|
||||||
|
"stage_settings": "~/.openflexure/stage_settings.json",
|
||||||
|
"plugins": [
|
||||||
|
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# Field of view, in stage steps
|
|
||||||
fov: [4100, 3146]
|
|
||||||
|
|
||||||
# Capture quality
|
|
||||||
jpeg_quality: 75
|
|
||||||
|
|
||||||
# Parameters specific to camera objects
|
|
||||||
camera_settings: ~/.openflexure/camera_settings.yaml
|
|
||||||
|
|
||||||
# Parameters specific to stage objects
|
|
||||||
stage_settings: ~/.openflexure/stage_settings.yaml
|
|
||||||
|
|
||||||
# Default plugins
|
|
||||||
plugins:
|
|
||||||
- openflexure_microscope.plugins.default.autofocus:AutofocusPlugin
|
|
||||||
- openflexure_microscope.plugins.default.scan:ScanPlugin
|
|
||||||
- openflexure_microscope.plugins.default.camera_calibration:Plugin
|
|
||||||
|
|
@ -23,6 +23,10 @@ class BaseStage(metaclass=ABCMeta):
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
"""(Optional) Save any settings to disk that need to be stored"""
|
||||||
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue