Draft of new semantics
This commit is contained in:
parent
b39a0653f2
commit
19af98736d
13 changed files with 252 additions and 187 deletions
|
|
@ -3,38 +3,9 @@ from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Import device modules
|
|
||||||
# NB this will eventually be handled by the RC file, so you can choose what device
|
|
||||||
# class should be attached.
|
|
||||||
try:
|
|
||||||
from openflexure_microscope.camera.pi import PiCameraStreamer
|
|
||||||
except ImportError:
|
|
||||||
logging.warning("Unable to import PiCameraStreamer")
|
|
||||||
from openflexure_microscope.camera.mock import MockStreamer
|
|
||||||
|
|
||||||
from openflexure_microscope.stage.sanga import SangaStage
|
|
||||||
from openflexure_microscope.stage.mock import MockStage
|
|
||||||
|
|
||||||
default_microscope = Microscope()
|
default_microscope = Microscope()
|
||||||
|
|
||||||
# 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...")
|
|
||||||
|
|
||||||
api_stage = MockStage()
|
|
||||||
|
|
||||||
# Attach devices to microscope
|
|
||||||
logging.debug("Attaching devices to microscope...")
|
|
||||||
default_microscope.attach(api_camera, api_stage)
|
|
||||||
|
|
||||||
# Restore loaded capture array to camera object
|
# Restore loaded capture array to camera object
|
||||||
logging.debug("Restoring captures...")
|
logging.debug("Restoring captures...")
|
||||||
default_microscope.camera.images = build_captures_from_exif(
|
default_microscope.camera.images = build_captures_from_exif(
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class SettingsProperty(View):
|
||||||
logging.debug("Updating settings from PUT request:")
|
logging.debug("Updating settings from PUT request:")
|
||||||
logging.debug(payload.json)
|
logging.debug(payload.json)
|
||||||
|
|
||||||
microscope.apply_settings(payload.json)
|
microscope.update_settings(payload.json)
|
||||||
microscope.save_settings()
|
microscope.save_settings()
|
||||||
|
|
||||||
return self.get()
|
return self.get()
|
||||||
|
|
@ -65,7 +65,7 @@ class NestedSettingsProperty(View):
|
||||||
dictionary = create_from_path(keys)
|
dictionary = create_from_path(keys)
|
||||||
set_by_path(dictionary, keys, payload.json)
|
set_by_path(dictionary, keys, payload.json)
|
||||||
|
|
||||||
microscope.apply_settings(dictionary)
|
microscope.update_settings(dictionary)
|
||||||
microscope.save_settings()
|
microscope.save_settings()
|
||||||
|
|
||||||
return self.get(route)
|
return self.get(route)
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,8 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.stream_timeout = 20
|
self.stream_timeout = 20
|
||||||
self.stream_timeout_enabled = False
|
self.stream_timeout_enabled = False
|
||||||
|
|
||||||
self.status = {"board": None}
|
self.stream_active = False
|
||||||
|
self.record_active = False
|
||||||
|
|
||||||
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
|
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
|
||||||
|
|
||||||
|
|
@ -129,8 +130,24 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.images = []
|
self.images = []
|
||||||
self.videos = []
|
self.videos = []
|
||||||
|
|
||||||
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def apply_settings(self, config: dict):
|
def configuration(self):
|
||||||
|
"""The current camera configuration."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def state(self):
|
||||||
|
"""The current read-only camera state."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings(self):
|
||||||
|
return self.read_settings()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Apply valid config params to camera object
|
# Apply valid config params to camera object
|
||||||
|
|
@ -187,7 +204,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.last_access = time.time()
|
self.last_access = time.time()
|
||||||
self.stop = False
|
self.stop = False
|
||||||
|
|
||||||
if not self.status["stream_active"]:
|
if not self.stream_active:
|
||||||
# start background frame thread
|
# start background frame thread
|
||||||
self.thread = threading.Thread(target=self._thread)
|
self.thread = threading.Thread(target=self._thread)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
|
|
@ -207,12 +224,12 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
logging.debug("Stopping worker thread")
|
logging.debug("Stopping worker thread")
|
||||||
timeout_time = time.time() + timeout
|
timeout_time = time.time() + timeout
|
||||||
|
|
||||||
if self.status["stream_active"]:
|
if self.stream_active:
|
||||||
self.stop = True
|
self.stop = True
|
||||||
self.thread.join() # Wait for stream thread to exit
|
self.thread.join() # Wait for stream thread to exit
|
||||||
logging.debug("Waiting for stream thread to exit.")
|
logging.debug("Waiting for stream thread to exit.")
|
||||||
|
|
||||||
while self.status["stream_active"]:
|
while self.stream_active:
|
||||||
if time.time() > timeout_time:
|
if time.time() > timeout_time:
|
||||||
logging.debug("Timeout waiting for worker thread close.")
|
logging.debug("Timeout waiting for worker thread close.")
|
||||||
raise TimeoutError("Timeout waiting for worker thread close.")
|
raise TimeoutError("Timeout waiting for worker thread close.")
|
||||||
|
|
@ -352,7 +369,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.frames_iterator = self.frames()
|
self.frames_iterator = self.frames()
|
||||||
logging.debug("Entering worker thread.")
|
logging.debug("Entering worker thread.")
|
||||||
|
|
||||||
self.status["stream_active"] = True
|
self.stream_active = True
|
||||||
|
|
||||||
for frame in self.frames_iterator:
|
for frame in self.frames_iterator:
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
|
|
@ -365,9 +382,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
and ( # If using timeout
|
and ( # If using timeout
|
||||||
time.time() - self.last_access > self.stream_timeout
|
time.time() - self.last_access > self.stream_timeout
|
||||||
)
|
)
|
||||||
and not self.status[ # And timeout time
|
and not self.preview_active # And GPU preview is not active
|
||||||
"preview_active"
|
|
||||||
] # And GPU preview is not active
|
|
||||||
):
|
):
|
||||||
self.frames_iterator.close()
|
self.frames_iterator.close()
|
||||||
break
|
break
|
||||||
|
|
@ -383,4 +398,4 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
logging.debug("BaseCamera worker thread exiting...")
|
logging.debug("BaseCamera worker thread exiting...")
|
||||||
# Set stream_activate state
|
# Set stream_activate state
|
||||||
self.status["stream_active"] = False
|
self.stream_active = False
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,13 @@ We override the logging settings in api.app by setting a level for PIL here.
|
||||||
pil_logger = logging.getLogger("PIL")
|
pil_logger = logging.getLogger("PIL")
|
||||||
pil_logger.setLevel(logging.INFO)
|
pil_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class MockStreamer(BaseCamera):
|
class MissingCamera(BaseCamera):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Run BaseCamera init
|
# Run BaseCamera init
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
|
|
||||||
# Store state of PiCameraStreamer
|
|
||||||
self.status.update(
|
|
||||||
{"stream_active": False, "record_active": False, "board": None}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update config properties
|
# Update config properties
|
||||||
self.image_resolution = (1312, 976)
|
self.image_resolution = (1312, 976)
|
||||||
self.stream_resolution = (640, 480)
|
self.stream_resolution = (640, 480)
|
||||||
|
|
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
|
||||||
|
|
||||||
image.save(self.stream, format="JPEG")
|
image.save(self.stream, format="JPEG")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configuration(self):
|
||||||
|
"""The current camera configuration."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""The current read-only camera state."""
|
||||||
|
return {}
|
||||||
|
|
||||||
def initialisation(self):
|
def initialisation(self):
|
||||||
"""Run any initialisation code when the frame iterator starts."""
|
"""Run any initialisation code when the frame iterator starts."""
|
||||||
pass
|
pass
|
||||||
|
|
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
||||||
def apply_settings(self, config: dict):
|
def update_settings(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Write a config dictionary to the PiCameraStreamer config.
|
Write a config dictionary to the PiCameraStreamer config.
|
||||||
|
|
||||||
|
|
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to camera object
|
# Apply valid config params to camera object
|
||||||
if not self.status["record_active"]: # If not recording a video
|
if not self.record_active: # If not recording a video
|
||||||
|
|
||||||
for key, value in config.items(): # For each provided setting
|
for key, value in config.items(): # For each provided setting
|
||||||
if hasattr(self, key):
|
if hasattr(self, key):
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ from .base import BaseCamera, CaptureObject
|
||||||
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.paths import settings_file_path
|
from openflexure_microscope.paths import settings_file_path
|
||||||
|
from openflexure_microscope.utilities import serialise_array_b64
|
||||||
|
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
|
|
@ -85,14 +86,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
) #: :py:class:`picamera.PiCamera`: Picamera object
|
) #: :py:class:`picamera.PiCamera`: Picamera object
|
||||||
|
|
||||||
# Store status of PiCameraStreamer
|
# Store status of PiCameraStreamer
|
||||||
self.status.update(
|
self.preview_active = False
|
||||||
{
|
|
||||||
"stream_active": False,
|
|
||||||
"record_active": False,
|
|
||||||
"preview_active": False,
|
|
||||||
"board": f"picamera_{self.camera.revision}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reset variable states
|
# Reset variable states
|
||||||
self.set_zoom(1.0)
|
self.set_zoom(1.0)
|
||||||
|
|
@ -116,15 +110,37 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"picamera_lst.npy"
|
"picamera_lst.npy"
|
||||||
) #: str: Path of .npy lens shading table file
|
) #: str: Path of .npy lens shading table file
|
||||||
|
|
||||||
# Update board identifier
|
|
||||||
self.status.update({})
|
|
||||||
|
|
||||||
# Create an empty stream
|
# Create an empty stream
|
||||||
self.stream = io.BytesIO()
|
self.stream = io.BytesIO()
|
||||||
|
|
||||||
# Start streaming
|
# Start streaming
|
||||||
self.start_worker()
|
self.start_worker()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configuration(self):
|
||||||
|
"""The current camera configuration."""
|
||||||
|
config = {
|
||||||
|
"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
|
||||||
|
def state(self):
|
||||||
|
"""The current read-only camera state."""
|
||||||
|
return {}
|
||||||
|
|
||||||
def initialisation(self):
|
def initialisation(self):
|
||||||
"""Run any initialisation code when the frame iterator starts."""
|
"""Run any initialisation code when the frame iterator starts."""
|
||||||
pass
|
pass
|
||||||
|
|
@ -176,7 +192,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
|
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
|
||||||
self.save_lens_shading_table()
|
self.save_lens_shading_table()
|
||||||
|
|
||||||
def apply_settings(self, config: dict):
|
def update_settings(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Write a config dictionary to the PiCameraStreamer config.
|
Write a config dictionary to the PiCameraStreamer config.
|
||||||
|
|
||||||
|
|
@ -194,10 +210,10 @@ class PiCameraStreamer(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to Picamera object
|
# Apply valid config params to Picamera object
|
||||||
if not self.status["record_active"]: # If not recording a video
|
if not self.record_active: # If not recording a video
|
||||||
|
|
||||||
# Pause stream while changing settings
|
# Pause stream while changing settings
|
||||||
if self.status["stream_active"]: # If stream is active
|
if self.stream_active: # If stream is active
|
||||||
logging.info("Pausing stream to update config.")
|
logging.info("Pausing stream to update config.")
|
||||||
self.stop_stream_recording() # Pause stream
|
self.stop_stream_recording() # Pause stream
|
||||||
paused_stream = True # Remember to unpause stream when done
|
paused_stream = True # Remember to unpause stream when done
|
||||||
|
|
@ -365,7 +381,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.preview.window = window
|
self.camera.preview.window = window
|
||||||
if fullscreen:
|
if fullscreen:
|
||||||
self.camera.preview.fullscreen = fullscreen
|
self.camera.preview.fullscreen = fullscreen
|
||||||
self.status["preview_active"] = True
|
self.preview_active = True
|
||||||
except picamera.exc.PiCameraMMALError as e:
|
except picamera.exc.PiCameraMMALError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
||||||
|
|
@ -380,7 +396,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
def stop_preview(self):
|
def stop_preview(self):
|
||||||
"""Stop the on board GPU camera preview."""
|
"""Stop the on board GPU camera preview."""
|
||||||
self.camera.stop_preview()
|
self.camera.stop_preview()
|
||||||
self.status["preview_active"] = False
|
self.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 recording.
|
||||||
|
|
@ -398,7 +414,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Start recording method only if a current recording is not running
|
# Start recording method only if a current recording is not running
|
||||||
if not self.status["record_active"]:
|
if not self.record_active:
|
||||||
|
|
||||||
# Start the camera video recording on port 2
|
# Start the camera video recording on port 2
|
||||||
logging.info("Recording to {}".format(output))
|
logging.info("Recording to {}".format(output))
|
||||||
|
|
@ -412,7 +428,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update status dictionary
|
# Update status dictionary
|
||||||
self.status["record_active"] = True
|
self.record_active = True
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
@ -432,7 +448,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
logging.info("Recording stopped")
|
logging.info("Recording stopped")
|
||||||
|
|
||||||
# Update status dictionary
|
# Update status dictionary
|
||||||
self.status["record_active"] = False
|
self.record_active = False
|
||||||
|
|
||||||
def stop_stream_recording(
|
def stop_stream_recording(
|
||||||
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
||||||
|
|
@ -500,7 +516,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.resolution = resolution
|
self.camera.resolution = resolution
|
||||||
|
|
||||||
# If the stream should be active
|
# If the stream should be active
|
||||||
if self.status["stream_active"]:
|
if self.stream_active:
|
||||||
try:
|
try:
|
||||||
# Start recording on stream port
|
# Start recording on stream port
|
||||||
self.camera.start_recording(
|
self.camera.start_recording(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from uuid import UUID
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
from .paths import CONFIG_FILE_PATH, DEFAULT_CONFIG_FILE_PATH
|
from .paths import SETTINGS_FILE_PATH, DEFAULT_SETTINGS_FILE_PATH, CONFIGURATION_FILE_PATH, DEFAULT_CONFIGURATION_FILE_PATH
|
||||||
|
|
||||||
|
|
||||||
class OpenflexureSettingsFile:
|
class OpenflexureSettingsFile:
|
||||||
|
|
@ -19,21 +19,21 @@ class OpenflexureSettingsFile:
|
||||||
expand (bool): Expand paths to valid auxillary config files.
|
expand (bool): Expand paths to valid auxillary config files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config_path: str = None):
|
def __init__(self, path: str, defaults: dict = {}):
|
||||||
global DEFAULT_CONFIG, CONFIG_FILE_PATH
|
global DEFAULT_SETTINGS
|
||||||
|
|
||||||
# Set arguments
|
# Set arguments
|
||||||
self.config_path = config_path or CONFIG_FILE_PATH
|
self.path = path
|
||||||
|
|
||||||
# Initialise basic config file with defaults if it doesn't exist
|
# Initialise basic config file with defaults if it doesn't exist
|
||||||
initialise_file(self.config_path, populate=DEFAULT_CONFIG)
|
initialise_file(self.path, populate=defaults)
|
||||||
|
|
||||||
def load(self) -> dict:
|
def load(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Loads settings from a file on-disk.
|
Loads settings from a file on-disk.
|
||||||
"""
|
"""
|
||||||
# Unexpanded config dictionary (used at load/save time)
|
# Unexpanded config dictionary (used at load/save time)
|
||||||
loaded_config = load_json_file(self.config_path)
|
loaded_config = load_json_file(self.path)
|
||||||
|
|
||||||
logging.debug("Reading settings from disk")
|
logging.debug("Reading settings from disk")
|
||||||
return loaded_config
|
return loaded_config
|
||||||
|
|
@ -50,11 +50,11 @@ class OpenflexureSettingsFile:
|
||||||
save_settings = config
|
save_settings = config
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
if os.path.isfile(self.config_path):
|
if os.path.isfile(self.path):
|
||||||
shutil.copyfile(self.config_path, self.config_path + ".bk")
|
shutil.copyfile(self.path, self.path + ".bk")
|
||||||
|
|
||||||
logging.debug("Saving settings dictionary to disk")
|
logging.debug("Saving settings dictionary to disk")
|
||||||
save_json_file(self.config_path, save_settings)
|
save_json_file(self.path, save_settings)
|
||||||
|
|
||||||
def merge(self, config: dict) -> dict:
|
def merge(self, config: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -178,10 +178,17 @@ def initialise_file(config_path, populate: str = "{}\n"):
|
||||||
outfile.write(populate)
|
outfile.write(populate)
|
||||||
|
|
||||||
|
|
||||||
# Load the default config
|
# Load the default settings
|
||||||
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc:
|
with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
|
||||||
DEFAULT_CONFIG = default_rc.read()
|
DEFAULT_SETTINGS = default_settings.read()
|
||||||
|
|
||||||
|
|
||||||
#: Default user settings object
|
#: Default user settings object
|
||||||
user_settings = OpenflexureSettingsFile(config_path=CONFIG_FILE_PATH)
|
user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS)
|
||||||
|
|
||||||
|
|
||||||
|
# Load the default configuration
|
||||||
|
with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
|
||||||
|
DEFAULT_CONFIGURATION = default_configuration.read()
|
||||||
|
|
||||||
|
#: Default user settings object
|
||||||
|
user_configuration = OpenflexureSettingsFile(path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION)
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,20 @@ import logging
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from openflexure_microscope.stage.base import BaseStage
|
from openflexure_microscope.stage.mock import MissingStage
|
||||||
from openflexure_microscope.stage.mock import MockStage
|
from openflexure_microscope.camera.mock import MissingCamera
|
||||||
from openflexure_microscope.camera.base import BaseCamera
|
from openflexure_microscope.stage.sanga import SangaStage
|
||||||
from openflexure_microscope.camera.mock import MockStreamer
|
try:
|
||||||
|
from openflexure_microscope.camera.pi import PiCameraStreamer
|
||||||
|
except ImportError:
|
||||||
|
logging.warning("Unable to import PiCameraStreamer")
|
||||||
|
from openflexure_microscope.camera.mock import MissingCamera
|
||||||
|
|
||||||
from openflexure_microscope.utilities import serialise_array_b64
|
from openflexure_microscope.utilities import serialise_array_b64
|
||||||
from openflexure_microscope.config import user_settings
|
from openflexure_microscope.config import user_settings, user_configuration
|
||||||
|
|
||||||
from labthings.core.lock import CompositeLock
|
from labthings.core.lock import CompositeLock
|
||||||
|
from labthings.core.utilities import rupdate
|
||||||
|
|
||||||
|
|
||||||
class Microscope:
|
class Microscope:
|
||||||
|
|
@ -24,20 +29,32 @@ 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):
|
def __init__(self, settings = user_settings, configuration = user_configuration):
|
||||||
# Initial attributes
|
# Store settings and configuration files
|
||||||
self.id = uuid.uuid4() #: Microscope UUID
|
self.settings_file = settings
|
||||||
self.name = self.id #: Microscope name (modifiable)
|
self.configuration_file = configuration
|
||||||
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
|
|
||||||
self.camera = None #: Currently connected camera object
|
|
||||||
self.stage = None #: Currently connected stage object
|
|
||||||
|
|
||||||
# Initialise with an empty composite lock
|
# Initialise with an empty composite lock
|
||||||
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
|
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
|
||||||
self.lock = CompositeLock([])
|
self.lock = CompositeLock([])
|
||||||
|
|
||||||
|
self.camera = None #: Currently connected camera object
|
||||||
|
self.stage = None #: Currently connected stage object
|
||||||
|
self.setup(self.configuration_file.load()) # Attach components
|
||||||
|
|
||||||
# Apply settings loaded from file
|
# Apply settings loaded from file
|
||||||
self.apply_settings(user_settings.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."""
|
||||||
|
|
@ -56,58 +73,49 @@ class Microscope:
|
||||||
self.stage.close()
|
self.stage.close()
|
||||||
logging.info("Closed {}".format(self))
|
logging.info("Closed {}".format(self))
|
||||||
|
|
||||||
def attach(self, camera: BaseCamera, stage: BaseStage):
|
def setup(self, configuration):
|
||||||
"""
|
"""
|
||||||
Retroactively attaches a camera and stage to the microscope object.
|
Attach microscope components based on initially passed configuration file
|
||||||
|
|
||||||
Allows the microscope to be created as a "dummy", with hardware communications
|
|
||||||
opened at a later time.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object
|
|
||||||
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
settings_full = self.read_settings()
|
### Detector
|
||||||
|
if configuration.get("detector"):
|
||||||
|
detector_type = configuration["detector"].get("type")
|
||||||
|
if detector_type == "PiCamera" or detector_type == "PiCameraStreamer":
|
||||||
|
try:
|
||||||
|
self.camera = PiCameraStreamer()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
logging.warning("No compatible camera hardware found.")
|
||||||
|
|
||||||
logging.debug("Attaching camera...")
|
### Stage
|
||||||
self.camera = (
|
if configuration.get("stage"):
|
||||||
camera
|
stage_type = configuration["stage"].get("type")
|
||||||
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object
|
stage_port = configuration["stage"].get("port")
|
||||||
|
if stage_type == "SangaBoard" or detector_type == "SangaStage":
|
||||||
|
try:
|
||||||
|
self.stage = SangaStage(port=stage_port)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
logging.warning("No compatible stage hardware found.")
|
||||||
|
|
||||||
|
### Fallbacks
|
||||||
if not self.camera:
|
if not self.camera:
|
||||||
logging.info("No camera attached.")
|
self.camera = MissingCamera()
|
||||||
else:
|
|
||||||
logging.info("Attached camera {}".format(camera))
|
|
||||||
|
|
||||||
if hasattr(self.camera, "lock"): # If camera has a lock
|
|
||||||
logging.info("Attaching {} to composite lock.".format(self.camera.lock))
|
|
||||||
# Add the lock to the microscope composite lock
|
|
||||||
self.lock.locks.append(self.camera.lock)
|
|
||||||
|
|
||||||
logging.debug("Attaching stage...")
|
|
||||||
self.stage = (
|
|
||||||
stage
|
|
||||||
) #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object
|
|
||||||
if not self.stage:
|
if not self.stage:
|
||||||
logging.info("No stage attached.")
|
self.stage = MissingStage()
|
||||||
else:
|
|
||||||
logging.info("Attached stage {}".format(stage))
|
|
||||||
|
|
||||||
if hasattr(self.stage, "lock"): # If stage object has a lock
|
### Locks
|
||||||
logging.info(
|
if hasattr(self.camera, "lock"):
|
||||||
"Attaching lock {} to composite lock.".format(self.stage.lock)
|
self.lock.locks.append(self.camera.lock)
|
||||||
)
|
if hasattr(self.stage, "lock"):
|
||||||
# Add the lock to the microscope composite lock
|
self.lock.locks.append(self.stage.lock)
|
||||||
self.lock.locks.append(self.stage.lock)
|
|
||||||
|
|
||||||
logging.info("Reapplying settings to newly attached devices")
|
|
||||||
self.apply_settings(settings_full)
|
|
||||||
|
|
||||||
def has_real_stage(self) -> bool:
|
def has_real_stage(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a real (non-mock) stage is currently attached.
|
Check if a real (non-mock) stage is currently attached.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "stage") and not isinstance(self.stage, MockStage):
|
if hasattr(self, "stage") and not isinstance(self.stage, MissingStage):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
@ -116,27 +124,26 @@ class Microscope:
|
||||||
"""
|
"""
|
||||||
Check if a real (non-mock) camera is currently attached.
|
Check if a real (non-mock) camera is currently attached.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "camera") and not isinstance(self.camera, MockStreamer):
|
if hasattr(self, "camera") and not isinstance(self.camera, MissingCamera):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create unified status
|
# Create unified status
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def state(self):
|
||||||
"""Dictionary of the basic microscope status.
|
"""Dictionary of the basic microscope status.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
dict: Dictionary containing complete microscope status
|
dict: Dictionary containing complete microscope status
|
||||||
"""
|
"""
|
||||||
state = {
|
state = {
|
||||||
"camera": self.camera.status,
|
"camera": self.camera.state,
|
||||||
"stage": self.stage.status,
|
"stage": self.stage.state,
|
||||||
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def apply_settings(self, config: dict):
|
def update_settings(self, config: 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.
|
||||||
"""
|
"""
|
||||||
|
|
@ -144,21 +151,19 @@ class Microscope:
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if ("camera_settings" in config) and self.camera:
|
if ("camera_settings" in config) and self.camera:
|
||||||
self.camera.apply_settings(config["camera_settings"])
|
self.camera.update_settings(config["camera_settings"])
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
if ("stage_settings" in config) and self.stage:
|
if ("stage_settings" in config) and self.stage:
|
||||||
self.stage.apply_settings(config["stage_settings"])
|
self.stage.update_settings(config["stage_settings"])
|
||||||
|
|
||||||
# Todo: tidy up with some loopy goodness
|
# Todo: tidy up with some loopy goodness
|
||||||
if "id" in config:
|
|
||||||
self.id = config["id"]
|
|
||||||
if "name" in config:
|
if "name" in config:
|
||||||
self.name = config["name"]
|
self.name = config["name"]
|
||||||
if "fov" in config:
|
if "fov" in config:
|
||||||
self.fov = config["fov"]
|
self.fov = config["fov"]
|
||||||
|
|
||||||
def read_settings(self, json_safe=False):
|
def read_settings(self, full: bool=True):
|
||||||
"""
|
"""
|
||||||
Get an updated settings dictionary.
|
Get an updated settings dictionary.
|
||||||
|
|
||||||
|
|
@ -169,7 +174,7 @@ class Microscope:
|
||||||
don't get removed from the settings file.
|
don't get removed from the settings file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
settings_current = {"id": self.id, "name": self.name, "fov": self.fov}
|
settings_current = {"name": self.name, "fov": self.fov}
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if self.camera:
|
if self.camera:
|
||||||
|
|
@ -181,9 +186,12 @@ class Microscope:
|
||||||
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"] = settings_current_stage
|
||||||
|
|
||||||
settings_full = user_settings.merge(settings_current)
|
settings_full = self.settings_file.merge(settings_current)
|
||||||
|
|
||||||
return settings_full
|
if full:
|
||||||
|
return settings_full
|
||||||
|
else:
|
||||||
|
return settings_current
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -196,7 +204,7 @@ class Microscope:
|
||||||
self.camera.save_settings()
|
self.camera.save_settings()
|
||||||
if self.stage:
|
if self.stage:
|
||||||
self.stage.save_settings()
|
self.stage.save_settings()
|
||||||
user_settings.save(current_config, backup=True)
|
self.settings_file.save(current_config, backup=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
|
|
@ -204,10 +212,10 @@ class Microscope:
|
||||||
Microscope system metadata, to be applied to basically all captures
|
Microscope system metadata, to be applied to basically all captures
|
||||||
"""
|
"""
|
||||||
system_metadata = {
|
system_metadata = {
|
||||||
"microscope_settings": self.read_settings(),
|
"@ID": self.id,
|
||||||
"microscope_state": self.status,
|
"settings": self.read_settings(full=False),
|
||||||
"microscope_id": self.id,
|
"state": self.state,
|
||||||
"microscope_name": self.name,
|
"configuration": self.configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store an encoded copy of the PiCamera lens shading table, if it exists
|
# Store an encoded copy of the PiCamera lens shading table, if it exists
|
||||||
|
|
@ -217,10 +225,32 @@ class Microscope:
|
||||||
|
|
||||||
b64_string, dtype, shape = serialise_array_b64(lst_arr)
|
b64_string, dtype, shape = serialise_array_b64(lst_arr)
|
||||||
|
|
||||||
system_metadata["lens_shading_table"] = {
|
system_metadata["configuration"]["detector"]["lens_shading_table"] = {
|
||||||
"b64_string": b64_string,
|
"b64_string": b64_string,
|
||||||
"dtype": dtype,
|
"dtype": dtype,
|
||||||
"shape": shape,
|
"shape": shape,
|
||||||
}
|
}
|
||||||
|
|
||||||
return system_metadata
|
return system_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configuration(self):
|
||||||
|
initial_configuration = self.configuration_file.load()
|
||||||
|
|
||||||
|
current_configuration = {
|
||||||
|
"@application": {
|
||||||
|
"name": "openflexure_microscope",
|
||||||
|
"version": pkg_resources.get_distribution("openflexure_microscope").version
|
||||||
|
},
|
||||||
|
"stage": {
|
||||||
|
"type": self.stage.__class__.__name__,
|
||||||
|
**self.stage.configuration
|
||||||
|
},
|
||||||
|
"detector": {
|
||||||
|
"type": self.camera.__class__.__name__,
|
||||||
|
**self.camera.configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initial_configuration.update(current_configuration)
|
||||||
|
return initial_configuration
|
||||||
|
|
|
||||||
17
openflexure_microscope/microscope_configuration.default.json
Normal file
17
openflexure_microscope/microscope_configuration.default.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"microscope": {
|
||||||
|
"stepsPerView": [4100, 3146]
|
||||||
|
},
|
||||||
|
"detector": {
|
||||||
|
"type": "PiCamera"
|
||||||
|
},
|
||||||
|
"stage": {
|
||||||
|
"type": "SangaStage",
|
||||||
|
"port": null
|
||||||
|
},
|
||||||
|
"lightSource": {
|
||||||
|
"type": "LED"
|
||||||
|
},
|
||||||
|
"objective": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
{
|
{
|
||||||
"fov": [
|
"jpeg_quality": 100
|
||||||
4100,
|
|
||||||
3146
|
|
||||||
],
|
|
||||||
"jpeg_quality": 75
|
|
||||||
}
|
}
|
||||||
|
|
@ -32,8 +32,9 @@ def logs_file_path(filename: str):
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
#: Path of default (first-run) microscope settings
|
#: Path of default (first-run) microscope settings
|
||||||
DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
|
DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
|
||||||
|
#: Path of default (first-run) microscope configuration
|
||||||
|
DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(HERE, "microscope_configuration.default.json")
|
||||||
|
|
||||||
# BASE PATHS
|
# BASE PATHS
|
||||||
|
|
||||||
|
|
@ -60,7 +61,9 @@ else:
|
||||||
|
|
||||||
# SERVER PATHS
|
# SERVER PATHS
|
||||||
|
|
||||||
#: Path of microscope settings directory
|
#: Path of microscope settings file
|
||||||
CONFIG_FILE_PATH = settings_file_path("microscope_settings.json")
|
SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json")
|
||||||
|
#: Path of microscope configuration file
|
||||||
|
CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json")
|
||||||
#: Path of microscope extensions directory
|
#: Path of microscope extensions directory
|
||||||
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")
|
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
|
||||||
self.lock = StrictLock(timeout=5)
|
self.lock = StrictLock(timeout=5)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def apply_settings(self, config: dict):
|
def update_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -29,12 +29,14 @@ class BaseStage(metaclass=ABCMeta):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def status(self):
|
def state(self):
|
||||||
"""The general state dictionary of the board.
|
"""The general state dictionary of the board."""
|
||||||
Should at least contain 'position', and 'board' keys.
|
pass
|
||||||
Note: A None/Null value for 'board' will disable stage
|
|
||||||
movement in the OpenFlexure eV client software,
|
@property
|
||||||
"""
|
@abstractmethod
|
||||||
|
def configuration(self):
|
||||||
|
"""The general stage configuration."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class MockStage(BaseStage):
|
class MissingStage(BaseStage):
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port=None, **kwargs):
|
||||||
BaseStage.__init__(self)
|
BaseStage.__init__(self)
|
||||||
self._position = [0, 0, 0]
|
self._position = [0, 0, 0]
|
||||||
|
|
@ -17,19 +17,17 @@ class MockStage(BaseStage):
|
||||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def state(self):
|
||||||
"""The general status dictionary of the board."""
|
"""The general status dictionary of the board."""
|
||||||
status = {
|
status = {"position": self.position_map}
|
||||||
"position": self.position_map,
|
|
||||||
"board": None,
|
|
||||||
"firmware": None,
|
|
||||||
"version": None,
|
|
||||||
}
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def apply_settings(self, config: dict):
|
@property
|
||||||
"""Update settings from a config dictionary"""
|
def configuration(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_settings(self, config: dict):
|
||||||
|
"""Update settings from a config dictionary"""
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
if "backlash" in config:
|
if "backlash" in config:
|
||||||
# Construct backlash array
|
# Construct backlash array
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class SangaStage(BaseStage):
|
||||||
"""Class managing serial communications with the motors for an Openflexure stage"""
|
"""Class managing serial communications with the motors for an Openflexure stage"""
|
||||||
BaseStage.__init__(self)
|
BaseStage.__init__(self)
|
||||||
|
|
||||||
|
self.port = port
|
||||||
self.board = Sangaboard(port, **kwargs)
|
self.board = Sangaboard(port, **kwargs)
|
||||||
|
|
||||||
self._backlash = (
|
self._backlash = (
|
||||||
|
|
@ -31,14 +32,17 @@ class SangaStage(BaseStage):
|
||||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def state(self):
|
||||||
"""The general status dictionary of the board."""
|
"""The general status dictionary of the board."""
|
||||||
status = {
|
return {"position": self.position_map}
|
||||||
"position": self.position_map,
|
|
||||||
|
@property
|
||||||
|
def configuration(self):
|
||||||
|
return {
|
||||||
|
"port": self.port,
|
||||||
"board": self.board.board,
|
"board": self.board.board,
|
||||||
"firmware": self.board.firmware,
|
"firmware": self.board.firmware,
|
||||||
}
|
}
|
||||||
return status
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_axes(self):
|
def n_axes(self):
|
||||||
|
|
@ -81,7 +85,7 @@ class SangaStage(BaseStage):
|
||||||
else:
|
else:
|
||||||
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||||
|
|
||||||
def apply_settings(self, config: dict):
|
def update_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
|
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue