Simplified new configuration and metadata

This commit is contained in:
Joel Collins 2020-01-29 15:36:57 +00:00
parent 5d65c62001
commit 7458d278d8
10 changed files with 214 additions and 216 deletions

View file

@ -19,7 +19,7 @@ from openflexure_microscope.paths import (
OPENFLEXURE_VAR_PATH, OPENFLEXURE_VAR_PATH,
OPENFLEXURE_EXTENSIONS_PATH, OPENFLEXURE_EXTENSIONS_PATH,
settings_file_path, settings_file_path,
logs_file_path logs_file_path,
) )
from labthings.server.quick import create_app from labthings.server.quick import create_app
@ -92,7 +92,7 @@ labthing.add_root_link(views.CaptureList, "captures")
labthing.add_view(views.CaptureView, f"/captures/<id>") labthing.add_view(views.CaptureView, f"/captures/<id>")
labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>") labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>")
labthing.add_view(views.CaptureTags, f"/captures/<id>/tags") labthing.add_view(views.CaptureTags, f"/captures/<id>/tags")
labthing.add_view(views.CaptureMetadata, f"/captures/<id>/metadata") labthing.add_view(views.CaptureAnnotations, f"/captures/<id>/annotations")
# Attach settings and status resources # Attach settings and status resources
labthing.add_view(views.SettingsProperty, f"/settings") labthing.add_view(views.SettingsProperty, f"/settings")
@ -101,6 +101,10 @@ labthing.add_view(views.NestedSettingsProperty, "/settings/<path:route>")
labthing.add_view(views.StatusProperty, "/status") labthing.add_view(views.StatusProperty, "/status")
labthing.add_view(views.NestedStatusProperty, "/status/<path:route>") labthing.add_view(views.NestedStatusProperty, "/status/<path:route>")
labthing.add_root_link(views.StatusProperty, "status") labthing.add_root_link(views.StatusProperty, "status")
labthing.add_view(views.ConfigurationProperty, "/configuration")
labthing.add_view(views.NestedConfigurationProperty, "/configuration/<path:route>")
labthing.add_root_link(views.ConfigurationProperty, "configuration")
# Attach streams resources # Attach streams resources
labthing.add_view(views.MjpegStream, f"/streams/mjpeg") labthing.add_view(views.MjpegStream, f"/streams/mjpeg")

View file

@ -1,6 +1,7 @@
import itertools import itertools
import logging import logging
import uuid import uuid
import datetime
from typing import Tuple from typing import Tuple
from functools import reduce from functools import reduce
@ -71,12 +72,12 @@ def progress():
def capture( def capture(
microscope, microscope,
basename, basename,
scan_id,
temporary: bool = False, temporary: bool = False,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
@ -94,16 +95,14 @@ def capture(
output.file, 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")
# Inject system metadata # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(metadata) output.put_metadata(metadata)
# Insert custom metadata
output.put_annotations(annotations)
# Insert custom tags # Insert custom tags
output.put_tags(tags) output.put_tags(tags)
@ -115,7 +114,7 @@ def tile(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
step_size: int = [2000, 1500, 100], stride_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5], grid: list = [3, 3, 5],
style="raster", style="raster",
autofocus_dz: int = 50, autofocus_dz: int = 50,
@ -124,13 +123,13 @@ def tile(
bayer: bool = False, bayer: bool = False,
fast_autofocus=False, fast_autofocus=False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_to_be_captured global _images_to_be_captured
global _images_captured_so_far global _images_captured_so_far
# Keep task progress # Keep task progress
# TODO: Make this line not nasty
_images_to_be_captured = reduce((lambda x, y: x * y), grid) _images_to_be_captured = reduce((lambda x, y: x * y), grid)
_images_captured_so_far = 0 _images_captured_so_far = 0
@ -138,28 +137,22 @@ def tile(
if not basename: if not basename:
basename = generate_basename() basename = generate_basename()
# Generate a stack ID
scan_id = uuid.uuid4()
# Store initial position # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
# Add scan metadata # Add dataset metadata
if "time" not in metadata: dataset_d = {
metadata["time"] = generate_basename() "dataset": {
"id": uuid.uuid4(),
metadata.update( "type": "xyzScan",
{ "name": basename,
"scan_id": scan_id, "acquisitionDate": datetime.datetime.now().isoformat(),
"basename": basename, "strideSize": stride_size,
"scan_parameters": { "grid": grid,
"step_size": step_size, "style": style,
"grid": grid, "autofocusDz": autofocus_dz,
"style": style,
"autofocus_dz": autofocus_dz,
},
} }
) }
# Check if autofocus is enabled # Check if autofocus is enabled
autofocus_extension = find_extension("org.openflexure.autofocus") autofocus_extension = find_extension("org.openflexure.autofocus")
@ -174,11 +167,11 @@ def tile(
autofocus_enabled = False autofocus_enabled = False
z_stack_dz = ( z_stack_dz = (
grid[2] * step_size[2] if grid[2] > 1 else 0 grid[2] * stride_size[2] if grid[2] > 1 else 0
) # shorthand for Z stack range ) # shorthand for Z stack range
# Construct an x-y grid (worry about z later) # Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style) x_y_grid = construct_grid(initial_position, stride_size[:2], grid[:2], style=style)
# Keep the initial Z position the same as our current position # Keep the initial Z position the same as our current position
next_z = initial_position[2] next_z = initial_position[2]
@ -209,26 +202,25 @@ def tile(
target_z=-z_stack_dz / 2.0, # Finish below the focus target_z=-z_stack_dz / 2.0, # Finish below the focus
initial_move_up=False, # We're already at the top of the scan initial_move_up=False, # We're already at the top of the scan
) )
# TODO: save the focus data for future reference? Use it for diagnostics?
else: else:
logging.debug("Running autofocus") logging.debug("Running autofocus")
autofocus_extension.autofocus( autofocus_extension.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
) )
logging.debug("Finished autofocus") logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove time.sleep(1)
# If we're not doing a z-stack, just capture # If we're not doing a z-stack, just capture
if grid[2] <= 1: if grid[2] <= 1:
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -240,15 +232,14 @@ def tile(
microscope=microscope, microscope=microscope,
basename=basename, basename=basename,
temporary=temporary, temporary=temporary,
scan_id=scan_id, step_size=stride_size[2],
step_size=step_size[2],
steps=grid[2], steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
return_to_start=not fast_autofocus, return_to_start=not fast_autofocus,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Make sure we use our current best estimate of focus (i.e. the current position) next point # Make sure we use our current best estimate of focus (i.e. the current position) next point
@ -259,7 +250,7 @@ def tile(
) # Fast autofocus requires us to start at the top of the range ) # Fast autofocus requires us to start at the top of the range
if grid[2] > 1: if grid[2] > 1:
next_z -= int( next_z -= int(
grid[2] / 2.0 * step_size[2] grid[2] / 2.0 * stride_size[2]
) # Z stacking means we're higher up to start with ) # Z stacking means we're higher up to start with
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to {}".format(initial_position))
@ -270,39 +261,25 @@ def stack(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
scan_id: str = None,
step_size: int = 100, step_size: int = 100,
steps: int = 5, steps: int = 5,
center: bool = True,
return_to_start: bool = True, return_to_start: bool = True,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_captured_so_far global _images_captured_so_far
# Generate a basename if none given
if not basename:
basename = generate_basename()
# Generate a stack ID
if not scan_id:
scan_id = uuid.uuid4()
# Add scan metadata
if not "time" in metadata:
metadata["time"] = generate_basename()
# Store initial position # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
with microscope.lock: with microscope.lock:
# Move to center scan # Move to center scan
if center: logging.debug("Moving to starting position")
logging.debug("Moving to starting position") microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
for i in range(steps): for i in range(steps):
time.sleep(0.1) time.sleep(0.1)
@ -310,12 +287,12 @@ def stack(
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -337,16 +314,18 @@ def stack(
class TileScanAPI(View): class TileScanAPI(View):
@use_args( @use_args(
{ {
"filename": fields.String(), "filename": fields.String(missing=None, example=None),
"temporary": fields.Boolean(missing=False), "temporary": fields.Boolean(missing=False),
"step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]), "stride_size": fields.List(
"grid": fields.List(fields.Integer, missing=[3, 3, 5]), fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
),
"grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
"style": fields.String(missing="raster"), "style": fields.String(missing="raster"),
"autofocus_dz": fields.Integer(missing=50), "autofocus_dz": fields.Integer(missing=50),
"fast_autofocus": fields.Boolean(missing=False), "fast_autofocus": fields.Boolean(missing=False),
"use_video_port": fields.Boolean(missing=False), "use_video_port": fields.Boolean(missing=False),
"bayer": fields.Boolean(missing=False), "bayer": fields.Boolean(missing=False),
"metadata": fields.Dict(missing={}), "annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
"tags": fields.List(fields.String, missing=[]), "tags": fields.List(fields.String, missing=[]),
"resize": fields.Dict(missing=None), # TODO: Validate keys "resize": fields.Dict(missing=None), # TODO: Validate keys
} }
@ -373,7 +352,7 @@ class TileScanAPI(View):
microscope, microscope,
basename=args.get("filename"), basename=args.get("filename"),
temporary=args.get("temporary"), temporary=args.get("temporary"),
step_size=args.get("step_size"), stride_size=args.get("stride_size"),
grid=args.get("grid"), grid=args.get("grid"),
style=args.get("style"), style=args.get("style"),
autofocus_dz=args.get("autofocus_dz"), autofocus_dz=args.get("autofocus_dz"),
@ -381,7 +360,7 @@ class TileScanAPI(View):
resize=resize, resize=resize,
bayer=args.get("bayer"), bayer=args.get("bayer"),
fast_autofocus=args.get("fast_autofocus"), fast_autofocus=args.get("fast_autofocus"),
metadata=args.get("metadata"), annotations=args.get("annotations"),
tags=args.get("tags"), tags=args.get("tags"),
) )

View file

@ -36,7 +36,7 @@ class CaptureAPI(View):
"bayer": fields.Boolean( "bayer": fields.Boolean(
missing=False, description="Store raw bayer data in file" missing=False, description="Store raw bayer data in file"
), ),
"metadata": fields.Dict(missing={}, example={"Client": "SwaggerUI"}), "annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
"tags": fields.List(fields.String, missing=[], example=["docs"]), "tags": fields.List(fields.String, missing=[], example=["docs"]),
"resize": fields.Dict( "resize": fields.Dict(
missing=None, example={"width": 640, "height": 480} missing=None, example={"width": 640, "height": 480}
@ -75,11 +75,10 @@ class CaptureAPI(View):
) )
# Inject system metadata # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(args.get("metadata")) output.put_annotations(args.get("annotations"))
# Insert custom tags # Insert custom tags
output.put_tags(args.get("tags")) output.put_tags(args.get("tags"))

View file

@ -39,10 +39,12 @@ class CaptureSchema(Schema):
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureTags), **description_from_view(CaptureTags),
}, },
"metadata": { "annotations": {
"href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True), "href": url_for(
CaptureAnnotations.endpoint, id=data.id, _external=True
),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureMetadata), **description_from_view(CaptureAnnotations),
}, },
"download": { "download": {
"href": url_for( "href": url_for(
@ -62,6 +64,9 @@ capture_schema = CaptureSchema()
capture_list_schema = CaptureSchema(many=True) capture_list_schema = CaptureSchema(many=True)
from pprint import pprint
@ThingProperty @ThingProperty
@Tag("captures") @Tag("captures")
class CaptureList(View): class CaptureList(View):
@ -197,10 +202,10 @@ class CaptureTags(View):
@Tag("captures") @Tag("captures")
class CaptureMetadata(View): class CaptureAnnotations(View):
def get(self, id): def get(self, id):
""" """
Get metadata associated with a single image capture Get annotations associated with a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.image_from_id(id)
@ -208,7 +213,7 @@ class CaptureMetadata(View):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)
def put(self, id): def put(self, id):
""" """
@ -226,7 +231,6 @@ class CaptureMetadata(View):
if type(data_dict) != dict: if type(data_dict) != dict:
return abort(400) return abort(400)
# TODO: Allow putting system metadata maybe? capture_obj.put_annotations(data_dict)
capture_obj.put_metadata(data_dict)
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)

View file

@ -78,7 +78,7 @@ class StatusProperty(View):
Show current read-only state of the microscope Show current read-only state of the microscope
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.status) return jsonify(microscope.state)
@Tag("properties") @Tag("properties")
@ -92,7 +92,35 @@ class NestedStatusProperty(View):
keys = route.split("/") keys = route.split("/")
try: try:
value = get_by_path(microscope.status, keys) value = get_by_path(microscope.state, keys)
except KeyError:
return abort(404)
return jsonify(value)
@ThingProperty
class ConfigurationProperty(View):
def get(self):
"""
Show current read-only state of the microscope
"""
microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.configuration)
@Tag("properties")
class NestedConfigurationProperty(View):
@doc_response(404, description="Configuration key cannot be found")
def get(self, route):
"""
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
try:
value = get_by_path(microscope.configuration, keys)
except KeyError: except KeyError:
return abort(404) return abort(404)

View file

@ -8,6 +8,7 @@ import yaml
import json import json
import logging import logging
from PIL import Image from PIL import Image
import dateutil.parser
import atexit import atexit
from openflexure_microscope.camera import piexif from openflexure_microscope.camera import piexif
@ -93,18 +94,18 @@ def capture_from_exif(path, exif_dict):
# Build file path information # Build file path information
capture.split_file_path(capture.file) capture.split_file_path(capture.file)
# Populate capture parameters # Image metadata
capture.id = exif_dict["id"] image_metadata = exif_dict.pop("image")
capture.timestring = exif_dict["time"]
capture.format = exif_dict["format"]
capture.custom_metadata = ( # Populate capture parameters
exif_dict["custom"] if "custom" in exif_dict.keys() else {} capture.id = image_metadata.get("id")
) capture.datetime = dateutil.parser.isoparse(image_metadata.get("acquisitionDate"))
capture.system_metadata = ( capture.format = image_metadata.get("format")
exif_dict["system"] if "system" in exif_dict.keys() else {} capture.tags = image_metadata.get("tags")
) capture.annotations = image_metadata.get("annotations")
capture.tags = exif_dict["tags"]
# Since we popped the "image" key, we dump whatever is left in _metadata
capture._metadata = exif_dict
return capture return capture
@ -113,16 +114,6 @@ class CaptureObject(object):
""" """
StreamObject used to store and process on-disk 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. Serves to simplify modifying properties of on-disk capture data.
Attributes:
timestring (str): Timestring of capture creation time
custom_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
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
""" """
def __init__(self, filepath) -> None: def __init__(self, filepath) -> None:
@ -131,17 +122,20 @@ class CaptureObject(object):
# Store a nice ID # Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID self.id = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created StreamObject {}".format(self.id)) logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") self.datetime = datetime.datetime.now()
# Create file name. Default to UUID # Create file name. Default to UUID
self.file = filepath self.file = filepath
self.split_file_path(self.file) self.split_file_path(self.file)
# Dictionary for storing custom metadata if not os.path.exists(self.filefolder):
self.custom_metadata = {} os.makedirs(self.filefolder)
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self.system_metadata = {}
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self._metadata = {}
# Dictionary for storing custom annotations
self.annotations = {}
# List for storing tags # List for storing tags
self.tags = [] self.tags = []
@ -164,10 +158,6 @@ class CaptureObject(object):
self.basename = os.path.splitext(self.filename)[0] self.basename = os.path.splitext(self.filename)[0]
self.format = self.filename.split(".")[-1] self.format = self.filename.split(".")[-1]
# Create folder and file
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
@property @property
def exists(self) -> bool: def exists(self) -> bool:
"""Check if capture data file exists on disk.""" """Check if capture data file exists on disk."""
@ -204,17 +194,24 @@ class CaptureObject(object):
# HANDLE METADATA # HANDLE METADATA
def put_metadata(self, data: dict, system: bool = False) -> None: def put_annotations(self, data: dict) -> None:
""" """
Merge metadata from a passed dictionary into the capture metadata, and saves. Merge annotations from a passed dictionary into the capture metadata, and saves.
Args: Args:
data (dict): Dictionary of metadata to be added data (dict): Dictionary of metadata to be added
""" """
if system: self.annotations.update(data)
self.system_metadata.update(data) self.save_metadata()
else:
self.custom_metadata.update(data) def put_metadata(self, data: dict) -> None:
"""
Merge root metadata from a passed dictionary into the capture metadata, and saves.
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata.update(data)
self.save_metadata() self.save_metadata()
def save_metadata(self) -> None: def save_metadata(self) -> None:
@ -244,12 +241,15 @@ class CaptureObject(object):
and any added custom metadata and tags. and any added custom metadata and tags.
""" """
d = { d = {
"id": self.id, "image": {
"time": self.timestring, "id": self.id,
"format": self.format, "name": self.filename,
"tags": self.tags, "acquisitionDate": self.datetime.isoformat(),
"custom": self.custom_metadata, "format": self.format,
"system": self.system_metadata, "tags": self.tags,
"annotations": self.annotations,
},
**self._metadata,
} }
# Add custom metadata to dictionary # Add custom metadata to dictionary

View file

@ -119,22 +119,7 @@ class PiCameraStreamer(BaseCamera):
@property @property
def configuration(self): def configuration(self):
"""The current camera configuration.""" """The current camera configuration."""
config = { return {"board": self.camera.revision}
"board": self.camera.revision,
}
if self.read_lens_shading_table():
b64_string, dtype, shape = serialise_array_b64(self.read_lens_shading_table())
config.update({
"lens_shading_table": {
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
})
return config
@property @property
def state(self): def state(self):

View file

@ -1,4 +1,5 @@
import json import json
import flask
import os import os
import errno import errno
import logging import logging
@ -7,7 +8,12 @@ from uuid import UUID
import numpy as np import numpy as np
from fractions import Fraction from fractions import Fraction
from .paths import SETTINGS_FILE_PATH, DEFAULT_SETTINGS_FILE_PATH, CONFIGURATION_FILE_PATH, DEFAULT_CONFIGURATION_FILE_PATH from .paths import (
SETTINGS_FILE_PATH,
DEFAULT_SETTINGS_FILE_PATH,
CONFIGURATION_FILE_PATH,
DEFAULT_CONFIGURATION_FILE_PATH,
)
class OpenflexureSettingsFile: class OpenflexureSettingsFile:
@ -71,7 +77,7 @@ class OpenflexureSettingsFile:
return settings return settings
class JSONEncoder(json.JSONEncoder): class JSONEncoder(flask.json.JSONEncoder):
""" """
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
""" """
@ -183,7 +189,9 @@ with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
DEFAULT_SETTINGS = default_settings.read() DEFAULT_SETTINGS = default_settings.read()
#: Default user settings object #: Default user settings object
user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS) user_settings = OpenflexureSettingsFile(
path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS
)
# Load the default configuration # Load the default configuration
@ -191,4 +199,7 @@ with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
DEFAULT_CONFIGURATION = default_configuration.read() DEFAULT_CONFIGURATION = default_configuration.read()
#: Default user settings object #: Default user settings object
user_configuration = OpenflexureSettingsFile(path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION) user_configuration = OpenflexureSettingsFile(
path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION
)

View file

@ -9,6 +9,7 @@ import uuid
from openflexure_microscope.stage.mock import MissingStage from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.camera.mock import MissingCamera from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.stage.sanga import SangaStage from openflexure_microscope.stage.sanga import SangaStage
try: try:
from openflexure_microscope.camera.pi import PiCameraStreamer from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError: except ImportError:
@ -29,7 +30,12 @@ class Microscope:
The camera and stage objects may already be initialised, and can be passed as arguments. The camera and stage objects may already be initialised, and can be passed as arguments.
""" """
def __init__(self, settings = user_settings, configuration = user_configuration): def __init__(self, settings=user_settings, configuration=user_configuration):
self.id = uuid.uuid4()
self.name = self.id
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
# Store settings and configuration files # Store settings and configuration files
self.settings_file = settings self.settings_file = settings
self.configuration_file = configuration self.configuration_file = configuration
@ -45,17 +51,6 @@ class Microscope:
# Apply settings loaded from file # Apply settings loaded from file
self.update_settings(self.settings_file.load()) self.update_settings(self.settings_file.load())
# Initial attributes
if self.configuration_file.load().get("id"):
self.id = configuration.get("id")
else:
self.id = uuid.uuid4()
self.configuration_file.save({
"id": self.id
})
self.name = self.id
def __enter__(self): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
return self return self
@ -79,9 +74,9 @@ class Microscope:
""" """
### Detector ### Detector
if configuration.get("detector"): if configuration.get("camera"):
detector_type = configuration["detector"].get("type") camera_type = configuration["camera"].get("type")
if detector_type == "PiCamera" or detector_type == "PiCameraStreamer": if camera_type == "PiCamera" or camera_type == "PiCameraStreamer":
try: try:
self.camera = PiCameraStreamer() self.camera = PiCameraStreamer()
except Exception as e: except Exception as e:
@ -92,7 +87,7 @@ class Microscope:
if configuration.get("stage"): if configuration.get("stage"):
stage_type = configuration["stage"].get("type") stage_type = configuration["stage"].get("type")
stage_port = configuration["stage"].get("port") stage_port = configuration["stage"].get("port")
if stage_type == "SangaBoard" or detector_type == "SangaStage": if stage_type == "SangaBoard" or camera_type == "SangaStage":
try: try:
self.stage = SangaStage(port=stage_port) self.stage = SangaStage(port=stage_port)
except Exception as e: except Exception as e:
@ -137,33 +132,32 @@ class Microscope:
Return: Return:
dict: Dictionary containing complete microscope status dict: Dictionary containing complete microscope status
""" """
state = { state = {"camera": self.camera.state, "stage": self.stage.state}
"camera": self.camera.state,
"stage": self.stage.state,
}
return state return state
def update_settings(self, config: dict): def update_settings(self, settings: dict):
""" """
Applies a settings dictionary to the microscope. Missing parameters will be left untouched. Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
""" """
logging.debug("Microscope: Applying config: {}".format(config)) logging.debug("Microscope: Applying settings: {}".format(settings))
# If attached to a camera # If attached to a camera
if ("camera_settings" in config) and self.camera: if ("camera" in settings) and self.camera:
self.camera.update_settings(config["camera_settings"]) self.camera.update_settings(settings["camera"])
# If attached to a stage # If attached to a stage
if ("stage_settings" in config) and self.stage: if ("stage" in settings) and self.stage:
self.stage.update_settings(config["stage_settings"]) self.stage.update_settings(settings["stage"])
# Todo: tidy up with some loopy goodness # Microscope settings
if "name" in config: if "id" in settings:
self.name = config["name"] self.id = settings["id"]
if "fov" in config: if "name" in settings:
self.fov = config["fov"] self.name = settings["name"]
if "fov" in settings:
self.fov = settings["fov"]
def read_settings(self, full: bool=True): def read_settings(self, full: bool = True):
""" """
Get an updated settings dictionary. Get an updated settings dictionary.
@ -174,17 +168,30 @@ class Microscope:
don't get removed from the settings file. don't get removed from the settings file.
""" """
settings_current = {"name": self.name, "fov": self.fov} settings_current = {"id": self.id, "name": self.name, "fov": self.fov}
# If attached to a camera # If attached to a camera
if self.camera: if self.camera:
settings_current_camera = self.camera.read_settings() settings_current_camera = self.camera.read_settings()
settings_current["camera_settings"] = settings_current_camera settings_current["camera"] = settings_current_camera
# Store an encoded copy of the PiCamera lens shading table, if it exists
if hasattr(self.camera, "read_lens_shading_table"):
# Read LST. Returns None if no LST is active
lst_arr = self.camera.read_lens_shading_table()
b64_string, dtype, shape = serialise_array_b64(lst_arr)
settings_current["camera"]["lens_shading_table"] = {
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
# If attached to a stage # If attached to a stage
if self.stage: if self.stage:
settings_current_stage = self.stage.read_settings() settings_current_stage = self.stage.read_settings()
settings_current["stage_settings"] = settings_current_stage settings_current["stage"] = settings_current_stage
settings_full = self.settings_file.merge(settings_current) settings_full = self.settings_file.merge(settings_current)
@ -206,33 +213,6 @@ class Microscope:
self.stage.save_settings() self.stage.save_settings()
self.settings_file.save(current_config, backup=True) self.settings_file.save(current_config, backup=True)
@property
def metadata(self):
"""
Microscope system metadata, to be applied to basically all captures
"""
system_metadata = {
"@ID": self.id,
"settings": self.read_settings(full=False),
"state": self.state,
"configuration": self.configuration
}
# Store an encoded copy of the PiCamera lens shading table, if it exists
if self.camera and hasattr(self.camera, "read_lens_shading_table"):
# Read LST. Returns None if no LST is active
lst_arr = self.camera.read_lens_shading_table()
b64_string, dtype, shape = serialise_array_b64(lst_arr)
system_metadata["configuration"]["detector"]["lens_shading_table"] = {
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
return system_metadata
@property @property
def configuration(self): def configuration(self):
initial_configuration = self.configuration_file.load() initial_configuration = self.configuration_file.load()
@ -240,17 +220,33 @@ class Microscope:
current_configuration = { current_configuration = {
"@application": { "@application": {
"name": "openflexure_microscope", "name": "openflexure_microscope",
"version": pkg_resources.get_distribution("openflexure_microscope").version "version": pkg_resources.get_distribution(
"openflexure_microscope"
).version,
}, },
"stage": { "stage": {
"type": self.stage.__class__.__name__, "type": self.stage.__class__.__name__,
**self.stage.configuration **self.stage.configuration,
}, },
"detector": { "camera": {
"type": self.camera.__class__.__name__, "type": self.camera.__class__.__name__,
**self.camera.configuration **self.camera.configuration,
} },
} }
initial_configuration.update(current_configuration) initial_configuration.update(current_configuration)
return initial_configuration return initial_configuration
@property
def metadata(self):
"""
Microscope system metadata, to be applied to basically all captures
"""
system_metadata = {
"id": self.id,
"settings": self.read_settings(full=False),
"state": self.state,
"configuration": self.configuration,
}
return system_metadata

View file

@ -1,17 +1,9 @@
{ {
"microscope": { "camera": {
"stepsPerView": [4100, 3146]
},
"detector": {
"type": "PiCamera" "type": "PiCamera"
}, },
"stage": { "stage": {
"type": "SangaStage", "type": "SangaStage",
"port": null "port": null
},
"lightSource": {
"type": "LED"
},
"objective": {
} }
} }