Merge branch 'config-overhaul' into 'master'
Config overhaul See merge request openflexure/openflexure-microscope-server!23
This commit is contained in:
commit
fdbba08b66
8 changed files with 279 additions and 284 deletions
|
|
@ -22,6 +22,7 @@ from openflexure_microscope.config import USER_CONFIG_DIR
|
||||||
|
|
||||||
from openflexure_microscope.api.v1 import blueprints
|
from openflexure_microscope.api.v1 import blueprints
|
||||||
|
|
||||||
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -59,7 +60,7 @@ else:
|
||||||
root.setLevel(logging.getLogger("gunicorn.error").level)
|
root.setLevel(logging.getLogger("gunicorn.error").level)
|
||||||
|
|
||||||
# Create a dummy microscope object, with no hardware attachments
|
# Create a dummy microscope object, with no hardware attachments
|
||||||
api_microscope = Microscope(None, None)
|
api_microscope = Microscope()
|
||||||
|
|
||||||
# Rebuild the capture list
|
# Rebuild the capture list
|
||||||
# TODO: Offload to a thread?
|
# TODO: Offload to a thread?
|
||||||
|
|
@ -89,13 +90,15 @@ handler = JSONExceptionHandler(app)
|
||||||
@app.before_first_request
|
@app.before_first_request
|
||||||
def attach_microscope():
|
def attach_microscope():
|
||||||
# Create the microscope object globally (common to all spawned server threads)
|
# Create the microscope object globally (common to all spawned server threads)
|
||||||
global api_microscope, capture_list
|
global api_microscope, stored_image_list
|
||||||
logging.debug("First request made. Populating microscope with hardware...")
|
logging.debug("First request made. Populating microscope with hardware...")
|
||||||
|
|
||||||
logging.debug("Creating camera object...")
|
logging.debug("Creating camera object...")
|
||||||
api_camera = StreamingCamera(config=api_microscope.rc.read())
|
# TODO: Try except finally, like with stage
|
||||||
|
api_camera = StreamingCamera()
|
||||||
|
|
||||||
logging.debug("Creating stage object...")
|
logging.debug("Creating stage object...")
|
||||||
|
# TODO: Tidy this up. api_stage may be referenced before assignment. Use some form of Maybe monad?
|
||||||
try:
|
try:
|
||||||
api_stage = SangaStage()
|
api_stage = SangaStage()
|
||||||
except (SerialException, OSError) as e:
|
except (SerialException, OSError) as e:
|
||||||
|
|
@ -166,17 +169,27 @@ def err_log():
|
||||||
def cleanup():
|
def cleanup():
|
||||||
global api_microscope
|
global api_microscope
|
||||||
logging.debug("App teardown started...")
|
logging.debug("App teardown started...")
|
||||||
|
logging.debug("Settling...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Save config
|
# Save config
|
||||||
if api_microscope.rc:
|
logging.debug("Saving config for teardown...")
|
||||||
api_microscope.rc.save(backup=True)
|
api_microscope.save_config(backup=True)
|
||||||
|
|
||||||
|
logging.debug("Settling...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Close down the microscope
|
# Close down the microscope
|
||||||
|
logging.debug("Closing devices...")
|
||||||
api_microscope.close()
|
api_microscope.close()
|
||||||
|
|
||||||
|
logging.debug("Settling...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
logging.debug("App teardown complete.")
|
logging.debug("App teardown complete.")
|
||||||
|
|
||||||
|
|
||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
|
app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import threading
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from greenlet import getcurrent as get_ident
|
from greenlet import getcurrent as get_ident
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -43,6 +45,11 @@ def generate_numbered_basename(obj_list: list) -> str:
|
||||||
return basename
|
return basename
|
||||||
|
|
||||||
|
|
||||||
|
def shunt_captures(target_list: list):
|
||||||
|
for obj in target_list: # For each older capture
|
||||||
|
obj.shunt() # Shunt capture from memory to storage
|
||||||
|
|
||||||
|
|
||||||
class CameraEvent(object):
|
class CameraEvent(object):
|
||||||
"""
|
"""
|
||||||
A frame-signaller object used by any instances or subclasses of BaseCamera.
|
A frame-signaller object used by any instances or subclasses of BaseCamera.
|
||||||
|
|
@ -87,7 +94,7 @@ class CameraEvent(object):
|
||||||
self.events[get_ident()][0].clear()
|
self.events[get_ident()][0].clear()
|
||||||
|
|
||||||
|
|
||||||
class BaseCamera(object):
|
class BaseCamera(metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Base implementation of StreamingCamera.
|
Base implementation of StreamingCamera.
|
||||||
|
|
||||||
|
|
@ -101,7 +108,6 @@ class BaseCamera(object):
|
||||||
stream_timeout (int): Number of inactive seconds before timing out the stream
|
stream_timeout (int): Number of inactive seconds before timing out the stream
|
||||||
stream_timeout_enabled (bool): Enable or disable timing out the stream
|
stream_timeout_enabled (bool): Enable or disable timing out the stream
|
||||||
state (dict): Dictionary for capture state
|
state (dict): Dictionary for capture state
|
||||||
config (dict): Dictionary of base camera config
|
|
||||||
paths (dict): Dictionary of capture paths
|
paths (dict): Dictionary of capture paths
|
||||||
images (list): List of image capture objects
|
images (list): List of image capture objects
|
||||||
videos (list): List of video capture objects
|
videos (list): List of video capture objects
|
||||||
|
|
@ -115,12 +121,12 @@ class BaseCamera(object):
|
||||||
self.frame = None
|
self.frame = None
|
||||||
self.last_access = 0
|
self.last_access = 0
|
||||||
self.event = CameraEvent()
|
self.event = CameraEvent()
|
||||||
|
self.stop = False # Used to indicate that the stream loop should break
|
||||||
|
|
||||||
self.stream_timeout = 20
|
self.stream_timeout = 20
|
||||||
self.stream_timeout_enabled = False
|
self.stream_timeout_enabled = False
|
||||||
|
|
||||||
self.state = {}
|
self.state = {}
|
||||||
self.config = {}
|
|
||||||
self.paths = {
|
self.paths = {
|
||||||
'image': BASE_CAPTURE_PATH,
|
'image': BASE_CAPTURE_PATH,
|
||||||
'video': BASE_CAPTURE_PATH,
|
'video': BASE_CAPTURE_PATH,
|
||||||
|
|
@ -132,13 +138,15 @@ class BaseCamera(object):
|
||||||
self.images = []
|
self.images = []
|
||||||
self.videos = []
|
self.videos = []
|
||||||
|
|
||||||
def apply_config(self, config):
|
@abstractmethod
|
||||||
|
def apply_config(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
self.config.update(config)
|
pass
|
||||||
|
|
||||||
def read_config(self, config):
|
@abstractmethod
|
||||||
|
def read_config(self):
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
return self.config
|
pass
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Create camera on context enter."""
|
"""Create camera on context enter."""
|
||||||
|
|
@ -176,7 +184,7 @@ class BaseCamera(object):
|
||||||
|
|
||||||
self.last_access = time.time()
|
self.last_access = time.time()
|
||||||
self.stop = False
|
self.stop = False
|
||||||
|
|
||||||
if not self.state['stream_active']:
|
if not self.state['stream_active']:
|
||||||
# start background frame thread
|
# start background frame thread
|
||||||
self.thread = threading.Thread(target=self._thread)
|
self.thread = threading.Thread(target=self._thread)
|
||||||
|
|
@ -222,9 +230,10 @@ class BaseCamera(object):
|
||||||
|
|
||||||
return self.frame
|
return self.frame
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
def frames(self):
|
def frames(self):
|
||||||
"""Create generator that returns frames from the camera."""
|
"""Create generator that returns frames from the camera."""
|
||||||
raise RuntimeError('Must be implemented by subclasses.')
|
pass
|
||||||
|
|
||||||
# RETURNING CAPTURES
|
# RETURNING CAPTURES
|
||||||
|
|
||||||
|
|
@ -248,10 +257,6 @@ class BaseCamera(object):
|
||||||
|
|
||||||
# CREATING NEW CAPTURES
|
# CREATING NEW CAPTURES
|
||||||
|
|
||||||
def shunt_captures(self, target_list: list):
|
|
||||||
for obj in target_list: # For each older capture
|
|
||||||
obj.shunt() # Shunt capture from memory to storage
|
|
||||||
|
|
||||||
def new_image(
|
def new_image(
|
||||||
self,
|
self,
|
||||||
write_to_file: bool = True,
|
write_to_file: bool = True,
|
||||||
|
|
@ -268,6 +273,7 @@ class BaseCamera(object):
|
||||||
temporary (bool): Should the data be deleted after session ends.
|
temporary (bool): Should the data be deleted after session ends.
|
||||||
Creating the capture with a content manager sets this to true.
|
Creating the capture with a content manager sets this to true.
|
||||||
filename (str): Name of the stored file. Defaults to timestamp.
|
filename (str): Name of the stored file. Defaults to timestamp.
|
||||||
|
folder (str): Name of the folder in which to store the capture.
|
||||||
fmt (str): Format of the capture.
|
fmt (str): Format of the capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -291,7 +297,7 @@ class BaseCamera(object):
|
||||||
filepath=filepath)
|
filepath=filepath)
|
||||||
|
|
||||||
# Update capture list
|
# Update capture list
|
||||||
self.shunt_captures(self.images)
|
shunt_captures(self.images)
|
||||||
self.images.append(output)
|
self.images.append(output)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
@ -312,6 +318,7 @@ class BaseCamera(object):
|
||||||
temporary (bool): Should the data be deleted after session ends.
|
temporary (bool): Should the data be deleted after session ends.
|
||||||
Creating the capture with a content manager sets this to true.
|
Creating the capture with a content manager sets this to true.
|
||||||
filename (str): Name of the stored file. Defaults to timestamp.
|
filename (str): Name of the stored file. Defaults to timestamp.
|
||||||
|
folder (str): Name of the folder in which to store the capture.
|
||||||
fmt (str): Format of the capture.
|
fmt (str): Format of the capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -335,7 +342,7 @@ class BaseCamera(object):
|
||||||
filepath=filepath)
|
filepath=filepath)
|
||||||
|
|
||||||
# Update capture list
|
# Update capture list
|
||||||
self.shunt_captures(self.videos)
|
shunt_captures(self.videos)
|
||||||
self.videos.append(output)
|
self.videos.append(output)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Video port:
|
||||||
Splitter port 2: Video capture
|
Splitter port 2: Video capture
|
||||||
Splitter port 3: [Currently unused]
|
Splitter port 3: [Currently unused]
|
||||||
|
|
||||||
StreamingCamera streams at stream_resolution
|
StreamingCamera streams at video_resolution
|
||||||
Camera capture resolution set to stream_resolution in frames()
|
Camera capture resolution set to stream_resolution in frames()
|
||||||
Video port uses that resolution for everything. If a different resolution
|
Video port uses that resolution for everything. If a different resolution
|
||||||
is specified for video capture, this is handled by the resizer.
|
is specified for video capture, this is handled by the resizer.
|
||||||
|
|
@ -39,36 +39,23 @@ 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
|
||||||
|
|
||||||
# Handle config and picamera settings
|
|
||||||
CONFIG_KEYS = {
|
|
||||||
'stream_resolution': (832, 624),
|
|
||||||
'image_resolution': (2592, 1944), # Default for PiCamera v1. Overridden in __init__
|
|
||||||
'numpy_resolution': (1312, 976),
|
|
||||||
'jpeg_quality': 75,
|
|
||||||
'picamera_settings': {
|
|
||||||
'exposure_mode': None,
|
|
||||||
'awb_mode': None,
|
|
||||||
'awb_gains': None,
|
|
||||||
'framerate': None,
|
|
||||||
'shutter_speed': None,
|
|
||||||
'saturation': None,
|
|
||||||
'analog_gain': None,
|
|
||||||
'digital_gain': None,
|
|
||||||
'lens_shading_table': None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class StreamingCamera(BaseCamera):
|
class StreamingCamera(BaseCamera):
|
||||||
"""Raspberry Pi camera implementation of StreamingCamera.
|
"""Raspberry Pi camera implementation of StreamingCamera."""
|
||||||
|
picamera_settings_keys = [
|
||||||
Args:
|
'exposure_mode',
|
||||||
config (dict): Dictionary of config parameters to apply on init. If None, will default to basic config.
|
'analog_gain',
|
||||||
"""
|
'digital_gain',
|
||||||
def __init__(self, config: dict = None):
|
'shutter_speed',
|
||||||
global CONFIG_KEYS
|
'awb_gains',
|
||||||
|
'awb_mode',
|
||||||
|
'framerate',
|
||||||
|
'saturation',
|
||||||
|
'lens_shading_table'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
# Run BaseCamera init
|
# Run BaseCamera init
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
# Attach to Pi camera
|
# Attach to Pi camera
|
||||||
|
|
@ -83,17 +70,11 @@ class StreamingCamera(BaseCamera):
|
||||||
# Reset variable states
|
# Reset variable states
|
||||||
self.set_zoom(1.0)
|
self.set_zoom(1.0)
|
||||||
|
|
||||||
# Populate config and settings with all available keys
|
# Update config properties
|
||||||
self.config.update(CONFIG_KEYS)
|
self.image_resolution = tuple(self.camera.MAX_RESOLUTION)
|
||||||
|
self.stream_resolution = (832, 624)
|
||||||
# Update config based on PiCamera parameters
|
self.numpy_resolution = (1312, 976)
|
||||||
self.config.update({
|
self.jpeg_quality = 75
|
||||||
'image_resolution': tuple(self.camera.MAX_RESOLUTION)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Load config dictionary if passed
|
|
||||||
if config:
|
|
||||||
self.apply_config(config)
|
|
||||||
|
|
||||||
# Create an empty stream
|
# Create an empty stream
|
||||||
self.stream = io.BytesIO()
|
self.stream = io.BytesIO()
|
||||||
|
|
@ -114,40 +95,28 @@ class StreamingCamera(BaseCamera):
|
||||||
self.camera.close()
|
self.camera.close()
|
||||||
|
|
||||||
# HANDLE SETTINGS
|
# HANDLE SETTINGS
|
||||||
@property
|
|
||||||
def supports_lens_shading(self):
|
|
||||||
"""Determine whether the picamera module supports lens shading.
|
|
||||||
|
|
||||||
As of March 2018, picamera did not wrap the necessary MMAL commands to
|
|
||||||
set the lens shading table, or to write the value of analog or digital
|
|
||||||
gain. I have a forked version of the library that does support these.
|
|
||||||
For ease of use by people who don't want those features, this library
|
|
||||||
does not have a hard dependency on lens shading. However, we need to
|
|
||||||
check in some places whether it's available.
|
|
||||||
"""
|
|
||||||
return hasattr(self.camera, "lens_shading_table")
|
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
"""
|
"""
|
||||||
Return config dictionary of the StreamingCamera.
|
Return config dictionary of the StreamingCamera.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
conf_dict = {
|
conf_dict = {
|
||||||
|
'stream_resolution': self.stream_resolution,
|
||||||
|
'image_resolution': self.image_resolution,
|
||||||
|
'numpy_resolution': self.numpy_resolution,
|
||||||
|
'jpeg_quality': self.jpeg_quality,
|
||||||
'picamera_settings': {},
|
'picamera_settings': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# PiCamera parameters (obtained directly from PiCamera object)
|
# PiCamera parameters
|
||||||
for key, _ in CONFIG_KEYS['picamera_settings'].items():
|
for key in StreamingCamera.picamera_settings_keys:
|
||||||
try:
|
try:
|
||||||
value = getattr(self.camera, key)
|
value = getattr(self.camera, key)
|
||||||
|
logging.debug("Reading PiCamera().{}: {}".format(key, value))
|
||||||
conf_dict['picamera_settings'][key] = value
|
conf_dict['picamera_settings'][key] = value
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.debug("Unable to read PiCamera attribute {}".format(key))
|
logging.debug("Unable to read PiCamera attribute {}".format(key))
|
||||||
|
|
||||||
# StreamingCamera parameters (obtained from StreamingCamera _config)
|
|
||||||
for key in CONFIG_KEYS:
|
|
||||||
if (key != 'picamera_settings') and (key in self.config):
|
|
||||||
conf_dict[key] = self.config[key]
|
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
||||||
def apply_config(self, config: dict) -> None:
|
def apply_config(self, config: dict) -> None:
|
||||||
|
|
@ -160,49 +129,31 @@ class StreamingCamera(BaseCamera):
|
||||||
Args:
|
Args:
|
||||||
config (dict): Dictionary of config parameters.
|
config (dict): Dictionary of config parameters.
|
||||||
"""
|
"""
|
||||||
|
# TODO: Include timing and batching logic when applying PiCamera settings
|
||||||
|
|
||||||
paused_stream = False
|
paused_stream = False
|
||||||
|
logging.debug("StreamingCamera: Applying config:")
|
||||||
|
logging.debug(config)
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to Picamera object
|
# Apply valid config params to Picamera object
|
||||||
if not self.state['record_active']: # If not recording a video
|
if not self.state['record_active']: # If not recording a video
|
||||||
|
|
||||||
logging.debug("Applying config:")
|
|
||||||
logging.debug(config)
|
|
||||||
|
|
||||||
# Pause stream while changing settings
|
# Pause stream while changing settings
|
||||||
if self.state['stream_active']: # If stream is active
|
if self.state['stream_active']: # If stream is active
|
||||||
logging.info("Pausing stream to update config.")
|
logging.info("Pausing stream to update config.")
|
||||||
self.stop_stream_recording() # Pause stream
|
self.stop_stream_recording() # Pause stream
|
||||||
paused_stream = True # Remember to unpause stream when done
|
paused_stream = True # Remember to unpause stream when done
|
||||||
|
|
||||||
# PiCamera parameters (applied directly to PiCamera object)
|
# PiCamera parameters
|
||||||
if 'picamera_settings' in config: # If new settings are given
|
if 'picamera_settings' in config: # If new settings are given
|
||||||
for key, value in config['picamera_settings'].items(): # For each given setting
|
self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True)
|
||||||
if hasattr(self.camera, key):
|
|
||||||
self.config['picamera_settings'][key] = None # Add the key to the list of returned settings
|
|
||||||
logging.debug("Setting parameter {}: {}".format(key, config['picamera_settings'][key]))
|
|
||||||
|
|
||||||
# Handle special attributes:
|
# StreamingCamera parameters
|
||||||
if key == 'digital_gain':
|
|
||||||
set_digital_gain(self.camera, value)
|
|
||||||
elif key == 'analog_gain':
|
|
||||||
set_analog_gain(self.camera, value)
|
|
||||||
elif key == "shutter_speed":
|
|
||||||
# TODO: use types from CONFIG_KEYS? @jtc42
|
|
||||||
self.camera.shutter_speed = int(value)
|
|
||||||
else:
|
|
||||||
setattr(self.camera, key, value) # Write setting to camera
|
|
||||||
|
|
||||||
# StreamingCamera parameters (applied via StreamingCamera config)
|
|
||||||
for key, value in config.items(): # For each provided setting
|
for key, value in config.items(): # For each provided setting
|
||||||
if key != 'picamera_settings': # We already handled this
|
if (key != 'picamera_settings') and hasattr(self, key):
|
||||||
if key not in CONFIG_KEYS.keys():
|
setattr(self, key, value)
|
||||||
logging.warn("{} is not in the streaming camera settings dictionary - adding it.")
|
|
||||||
#continue #TODO: filter settings somehow?
|
|
||||||
logging.debug("Setting parameter {}: {}".format(key, value))
|
|
||||||
self.config[key] = value
|
|
||||||
|
|
||||||
# If stream was paused to update config, unpause
|
# If stream was paused to update config, unpause
|
||||||
if paused_stream:
|
if paused_stream:
|
||||||
|
|
@ -213,6 +164,53 @@ class StreamingCamera(BaseCamera):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Cannot update camera config while recording is active.")
|
"Cannot update camera config while recording is active.")
|
||||||
|
|
||||||
|
def apply_picamera_settings(self, settings_dict: dict, pause_for_effect: bool=True):
|
||||||
|
# Set exposure mode
|
||||||
|
if 'exposure_mode' in settings_dict:
|
||||||
|
logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode']))
|
||||||
|
self.camera.exposure_mode = settings_dict['exposure_mode']
|
||||||
|
|
||||||
|
# Apply gains and let them settle
|
||||||
|
if 'analog_gain' in settings_dict:
|
||||||
|
logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain']))
|
||||||
|
set_analog_gain(self.camera, settings_dict['analog_gain'])
|
||||||
|
if 'digital_gain' in settings_dict:
|
||||||
|
logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain']))
|
||||||
|
set_digital_gain(self.camera, settings_dict['digital_gain'])
|
||||||
|
|
||||||
|
# Apply shutter speed
|
||||||
|
if 'shutter_speed' in settings_dict:
|
||||||
|
logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed']))
|
||||||
|
self.camera.shutter_speed = settings_dict['shutter_speed']
|
||||||
|
|
||||||
|
time.sleep(0.2) # Let gains settle
|
||||||
|
|
||||||
|
# Handle AWB in a half-smart way
|
||||||
|
if 'awb_gains' in settings_dict:
|
||||||
|
logging.debug("Applying awb_mode: off")
|
||||||
|
self.camera.awb_mode = 'off'
|
||||||
|
logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains']))
|
||||||
|
self.camera.awb_gains = settings_dict['awb_gains']
|
||||||
|
elif 'awb_mode' in settings_dict:
|
||||||
|
logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode']))
|
||||||
|
self.camera.awb_mode = settings_dict['awb_mode']
|
||||||
|
|
||||||
|
# Handle some properties that can be quickly applied
|
||||||
|
batched_keys = ['framerate', 'saturation']
|
||||||
|
for key in batched_keys:
|
||||||
|
if (key in settings_dict) and hasattr(self.camera, key):
|
||||||
|
logging.debug("Applying {}: {}".format(key, settings_dict[key]))
|
||||||
|
setattr(self.camera, key, settings_dict[key])
|
||||||
|
|
||||||
|
# Handle lens shading if camera supports it
|
||||||
|
if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'):
|
||||||
|
logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table']))
|
||||||
|
self.camera.lens_shading_table = settings_dict['lens_shading_table']
|
||||||
|
|
||||||
|
# Final optional pause to settle
|
||||||
|
if pause_for_effect:
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
def set_zoom(self, zoom_value: float = 1.) -> None:
|
def set_zoom(self, zoom_value: float = 1.) -> None:
|
||||||
"""
|
"""
|
||||||
Change the camera zoom, handling re-centering and scaling.
|
Change the camera zoom, handling re-centering and scaling.
|
||||||
|
|
@ -223,21 +221,21 @@ class StreamingCamera(BaseCamera):
|
||||||
self.state['zoom_value'] = 1
|
self.state['zoom_value'] = 1
|
||||||
# Richard's code for zooming !
|
# Richard's code for zooming !
|
||||||
fov = self.camera.zoom
|
fov = self.camera.zoom
|
||||||
centre = np.array([fov[0] + fov[2]/2.0, fov[1] + fov[3]/2.0])
|
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
||||||
size = 1.0/self.state['zoom_value']
|
size = 1.0 / self.state['zoom_value']
|
||||||
# If the new zoom value would be invalid, move the centre to
|
# If the new zoom value would be invalid, move the centre to
|
||||||
# keep it within the camera's sensor (this is only relevant
|
# keep it within the camera's sensor (this is only relevant
|
||||||
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
if np.abs(centre[i] - 0.5) + size/2 > 0.5:
|
if np.abs(centre[i] - 0.5) + size / 2 > 0.5:
|
||||||
centre[i] = 0.5 + (1.0 - size)/2 * np.sign(centre[i]-0.5)
|
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
|
||||||
logging.info("setting zoom, centre {}, size {}".format(centre, size))
|
logging.info("setting zoom, centre {}, size {}".format(centre, size))
|
||||||
new_fov = (centre[0] - size/2, centre[1] - size/2, size, size)
|
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
|
||||||
self.camera.zoom = new_fov
|
self.camera.zoom = new_fov
|
||||||
|
|
||||||
# LAUNCH ACTIONS
|
# LAUNCH ACTIONS
|
||||||
|
|
||||||
def start_preview(self, fullscreen=True, window=None) -> bool:
|
def start_preview(self, fullscreen=True, window=None):
|
||||||
"""Start the on board GPU camera preview."""
|
"""Start the on board GPU camera preview."""
|
||||||
logging.info("Starting the GPU preview")
|
logging.info("Starting the GPU preview")
|
||||||
|
|
||||||
|
|
@ -260,7 +258,7 @@ class StreamingCamera(BaseCamera):
|
||||||
except picamera.exc.PiCameraValueError as e:
|
except picamera.exc.PiCameraValueError as e:
|
||||||
logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e))
|
logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e))
|
||||||
|
|
||||||
def stop_preview(self) -> bool:
|
def stop_preview(self):
|
||||||
"""Stop the on board GPU camera preview."""
|
"""Stop the on board GPU camera preview."""
|
||||||
self.camera.stop_preview()
|
self.camera.stop_preview()
|
||||||
self.state['preview_active'] = False
|
self.state['preview_active'] = False
|
||||||
|
|
@ -289,8 +287,6 @@ class StreamingCamera(BaseCamera):
|
||||||
|
|
||||||
# If output is a StreamObject
|
# If output is a StreamObject
|
||||||
if isinstance(output, CaptureObject):
|
if isinstance(output, CaptureObject):
|
||||||
# Lock the StreamObject while recording
|
|
||||||
output.lock()
|
|
||||||
# Set target to capture stream
|
# Set target to capture stream
|
||||||
output_stream = output.stream
|
output_stream = output.stream
|
||||||
else:
|
else:
|
||||||
|
|
@ -303,7 +299,7 @@ class StreamingCamera(BaseCamera):
|
||||||
output_stream,
|
output_stream,
|
||||||
format=fmt,
|
format=fmt,
|
||||||
splitter_port=2,
|
splitter_port=2,
|
||||||
resize=self.config['stream_resolution'],
|
resize=self.stream_resolution,
|
||||||
quality=quality)
|
quality=quality)
|
||||||
|
|
||||||
# Update state dictionary
|
# Update state dictionary
|
||||||
|
|
@ -342,7 +338,7 @@ class StreamingCamera(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# If no resolution is specified, default to image_resolution
|
# If no resolution is specified, default to image_resolution
|
||||||
if not resolution:
|
if not resolution:
|
||||||
resolution = self.config['image_resolution']
|
resolution = self.image_resolution
|
||||||
|
|
||||||
# Stop the camera video recording on port 1
|
# Stop the camera video recording on port 1
|
||||||
try:
|
try:
|
||||||
|
|
@ -366,7 +362,7 @@ class StreamingCamera(BaseCamera):
|
||||||
Args:
|
Args:
|
||||||
splitter_port (int): Splitter port to start recording on
|
splitter_port (int): Splitter port to start recording on
|
||||||
resolution ((int, int)): Resolution to set the camera to, before starting recording.
|
resolution ((int, int)): Resolution to set the camera to, before starting recording.
|
||||||
Defaults to `self.config['stream_resolution']`.
|
Defaults to `self.stream_resolution`.
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# If stream object was destroyed
|
# If stream object was destroyed
|
||||||
|
|
@ -375,7 +371,7 @@ class StreamingCamera(BaseCamera):
|
||||||
|
|
||||||
# If no explicit resolution is passed
|
# If no explicit resolution is passed
|
||||||
if not resolution:
|
if not resolution:
|
||||||
resolution = self.config['stream_resolution'] # Default to video recording resolution
|
resolution = self.stream_resolution # Default to video recording resolution
|
||||||
|
|
||||||
# Reduce the resolution for video streaming
|
# Reduce the resolution for video streaming
|
||||||
try:
|
try:
|
||||||
|
|
@ -392,7 +388,7 @@ class StreamingCamera(BaseCamera):
|
||||||
self.camera.start_recording(
|
self.camera.start_recording(
|
||||||
self.stream,
|
self.stream,
|
||||||
format='mjpeg',
|
format='mjpeg',
|
||||||
quality=self.config['jpeg_quality'],
|
quality=self.jpeg_quality,
|
||||||
bitrate=-1, # RWB: disable bitrate control
|
bitrate=-1, # RWB: disable bitrate control
|
||||||
# (bitrate control makes JPEG size less good as a focus
|
# (bitrate control makes JPEG size less good as a focus
|
||||||
# metric)
|
# metric)
|
||||||
|
|
@ -463,9 +459,9 @@ class StreamingCamera(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if use_video_port:
|
if use_video_port:
|
||||||
resolution = self.config['stream_resolution']
|
resolution = self.stream_resolution
|
||||||
else:
|
else:
|
||||||
resolution = self.config['numpy_resolution']
|
resolution = self.numpy_resolution
|
||||||
|
|
||||||
if resize:
|
if resize:
|
||||||
size = resize
|
size = resize
|
||||||
|
|
@ -503,9 +499,9 @@ class StreamingCamera(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if use_video_port:
|
if use_video_port:
|
||||||
resolution = self.config['stream_resolution']
|
resolution = self.stream_resolution
|
||||||
else:
|
else:
|
||||||
resolution = self.config['numpy_resolution']
|
resolution = self.numpy_resolution
|
||||||
|
|
||||||
if resize:
|
if resize:
|
||||||
size = resize
|
size = resize
|
||||||
|
|
@ -557,7 +553,7 @@ class StreamingCamera(BaseCamera):
|
||||||
self.stream.seek(0)
|
self.stream.seek(0)
|
||||||
self.stream.truncate()
|
self.stream.truncate()
|
||||||
# to stream, read the new frame
|
# to stream, read the new frame
|
||||||
time.sleep(1/self.camera.framerate*0.1)
|
time.sleep(1 / self.camera.framerate * 0.1)
|
||||||
# yield the result to be read
|
# yield the result to be read
|
||||||
frame = self.stream.getvalue()
|
frame = self.stream.getvalue()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool)
|
||||||
|
|
||||||
# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON
|
# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON
|
||||||
|
|
||||||
def json_convert(v):
|
def convert_type_to_json_safe(v):
|
||||||
"""Make an individual attribute JSON-safe"""
|
"""Make an individual attribute JSON-safe"""
|
||||||
if isinstance(v, Fraction):
|
if isinstance(v, Fraction):
|
||||||
return float(v)
|
return float(v)
|
||||||
|
|
@ -66,7 +66,7 @@ def json_convert(v):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
def to_map(data, func):
|
def recursively_apply(data, func):
|
||||||
"""
|
"""
|
||||||
Recursively apply a function to a dictionary, list, array, or tuple
|
Recursively apply a function to a dictionary, list, array, or tuple
|
||||||
|
|
||||||
|
|
@ -76,18 +76,18 @@ def to_map(data, func):
|
||||||
"""
|
"""
|
||||||
# If the object is a dictionary
|
# If the object is a dictionary
|
||||||
if isinstance(data, abc.Mapping):
|
if isinstance(data, abc.Mapping):
|
||||||
return {key: to_map(val, func) for key, val in data.items()}
|
return {key: recursively_apply(val, func) for key, val in data.items()}
|
||||||
# If the object is iterable but NOT a dictionary or a string
|
# If the object is iterable but NOT a dictionary or a string
|
||||||
elif (isinstance(data, abc.Iterable) and
|
elif (isinstance(data, abc.Iterable) and
|
||||||
not isinstance(data, abc.Mapping) and
|
not isinstance(data, abc.Mapping) and
|
||||||
not isinstance(data, str)):
|
not isinstance(data, str)):
|
||||||
return [to_map(x, func) for x in data]
|
return [recursively_apply(x, func) for x in data]
|
||||||
# if the object is neither a map nor iterable
|
# if the object is neither a map nor iterable
|
||||||
else:
|
else:
|
||||||
return func(data)
|
return func(data)
|
||||||
|
|
||||||
|
|
||||||
def json_map(data, clean_keys=True):
|
def settings_to_json(data: dict, clean_keys=True):
|
||||||
"""
|
"""
|
||||||
Make a copy of an input dictionary that's safe for JSON return
|
Make a copy of an input dictionary that's safe for JSON return
|
||||||
|
|
||||||
|
|
@ -102,12 +102,12 @@ def json_map(data, clean_keys=True):
|
||||||
# If we're cleaning up unsuitable keys
|
# If we're cleaning up unsuitable keys
|
||||||
if clean_keys:
|
if clean_keys:
|
||||||
# Convert lens_shading_table to a bool
|
# Convert lens_shading_table to a bool
|
||||||
if 'picamera_settings' in d and 'lens_shading_table' in d['picamera_settings']:
|
if 'picamera_settings' in d['camera_settings'] and 'lens_shading_table' in d['camera_settings']['picamera_settings']:
|
||||||
logging.debug("Bool-ifying lens_shading_table")
|
logging.debug("Bool-ifying lens_shading_table")
|
||||||
if d['picamera_settings']['lens_shading_table'] is not None:
|
if d['camera_settings']['picamera_settings']['lens_shading_table'] is not None:
|
||||||
d['picamera_settings']['lens_shading_table'] = True
|
d['camera_settings']['picamera_settings']['lens_shading_table'] = True
|
||||||
|
|
||||||
return to_map(d, json_convert)
|
return recursively_apply(d, convert_type_to_json_safe)
|
||||||
|
|
||||||
|
|
||||||
# HANDLE BASIC LOADING AND SAVING OF YAML FILES
|
# HANDLE BASIC LOADING AND SAVING OF YAML FILES
|
||||||
|
|
@ -142,6 +142,7 @@ def save_yaml_file(config_path: str, config_dict: dict, safe: bool = False):
|
||||||
config_path = os.path.expanduser(config_path)
|
config_path = os.path.expanduser(config_path)
|
||||||
|
|
||||||
logging.info("Saving {}...".format(config_path))
|
logging.info("Saving {}...".format(config_path))
|
||||||
|
logging.debug(config_dict)
|
||||||
|
|
||||||
with open(config_path, 'w') as outfile:
|
with open(config_path, 'w') as outfile:
|
||||||
if not safe:
|
if not safe:
|
||||||
|
|
@ -183,7 +184,7 @@ def initialise_file(config_path, populate: str = ""):
|
||||||
|
|
||||||
|
|
||||||
# MAIN CONFIG CLASS
|
# MAIN CONFIG CLASS
|
||||||
class OpenflexureConfig:
|
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.
|
||||||
|
|
||||||
|
|
@ -195,83 +196,57 @@ class OpenflexureConfig:
|
||||||
global DEFAULT_CONFIG, USER_CONFIG_FILE
|
global DEFAULT_CONFIG, USER_CONFIG_FILE
|
||||||
|
|
||||||
self.expandable_keys = {
|
self.expandable_keys = {
|
||||||
'picamera_settings': None,
|
'camera_settings': None,
|
||||||
'openflexure_stage_settings': None
|
'stage_settings': None
|
||||||
} #: Dictionary of keys that can be passed as a file path string and expanded automatically
|
} #: Dictionary of keys that can be passed as a file path string and expanded automatically
|
||||||
|
|
||||||
# Set arguments
|
# Set arguments
|
||||||
self.config_path = config_path or USER_CONFIG_FILE
|
self.config_path = config_path or USER_CONFIG_FILE
|
||||||
self.expand = expand
|
self.expand = expand
|
||||||
|
|
||||||
# Create empty config dictionaries
|
|
||||||
self._config = {}
|
|
||||||
|
|
||||||
# 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.config_path, populate=DEFAULT_CONFIG)
|
||||||
|
|
||||||
# Load the config in, setting self._config and self.config
|
|
||||||
self.load()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
"""
|
|
||||||
Return a dictionary representation of the current config as a property.
|
|
||||||
"""
|
|
||||||
return self.read()
|
|
||||||
|
|
||||||
def read(self, json_safe=False):
|
|
||||||
"""
|
|
||||||
Read the current full config.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
json_safe (bool): Converts invalid data types to JSON types, and removes any
|
|
||||||
excessively large values (e.g. lens-shading tables).
|
|
||||||
"""
|
|
||||||
if json_safe:
|
|
||||||
logging.info("Reading config as JSON-safe dictionary")
|
|
||||||
return json_map(self._config)
|
|
||||||
else:
|
|
||||||
logging.info("Reading config directly")
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
def write(self, update_dict: dict):
|
|
||||||
"""
|
|
||||||
Write properties to the config. Merges dictionaries, rather than fully replacing.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
update_dict (dict): Dictionary of config data to merge.
|
|
||||||
"""
|
|
||||||
self._config.update(update_dict)
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
self._config = load_yaml_file(self.config_path)
|
loaded_config = load_yaml_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:
|
||||||
# Expand self.raw_config into self._config
|
# Expand self.raw_config into self._config
|
||||||
self._config = self.expand_config(self._config)
|
loaded_config = self.expand_config(loaded_config)
|
||||||
|
|
||||||
def save(self, backup: bool = True):
|
logging.debug("Reading settings from disk")
|
||||||
|
return loaded_config
|
||||||
|
|
||||||
|
def save(self, config: dict, backup: bool = True):
|
||||||
"""
|
"""
|
||||||
Save config to a file on-disk, and splits into auxillary config files if available.
|
Save config to a file on-disk, and splits into auxillary config files if available.
|
||||||
"""
|
"""
|
||||||
# If the loaded config was in contracted format
|
# If the loaded config was in contracted format
|
||||||
if self.expand:
|
if self.expand:
|
||||||
# Contract self._config into self.raw_config
|
# Contract self._config into self.raw_config
|
||||||
save_config = self.contract_config(self._config)
|
save_config = self.contract_config(config)
|
||||||
else:
|
else:
|
||||||
save_config = self._config
|
save_config = config
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
if os.path.isfile(self.config_path):
|
if os.path.isfile(self.config_path):
|
||||||
shutil.copyfile(self.config_path, self.config_path+".bk")
|
shutil.copyfile(self.config_path, self.config_path+".bk")
|
||||||
|
|
||||||
|
logging.debug("Saving settings dictionary to disk")
|
||||||
save_yaml_file(self.config_path, save_config)
|
save_yaml_file(self.config_path, save_config)
|
||||||
|
|
||||||
|
def merge(self, config: dict, backup: bool = True):
|
||||||
|
logging.debug("Merging settings with file on disk")
|
||||||
|
settings = self.load()
|
||||||
|
settings.update(config)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
def expand_config(self, config_dict):
|
def expand_config(self, config_dict):
|
||||||
"""
|
"""
|
||||||
Search a config dictionary for paths to valid auxillary config files,
|
Search a config dictionary for paths to valid auxillary config files,
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,28 @@
|
||||||
Defines a microscope object, binding a camera and stage with basic functionality.
|
Defines a microscope object, binding a camera and stage with basic functionality.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import pkg_resources
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from openflexure_stage import OpenFlexureStage
|
from openflexure_stage import OpenFlexureStage
|
||||||
|
|
||||||
from .camera.pi import StreamingCamera
|
from .camera.pi import StreamingCamera
|
||||||
|
|
||||||
from .plugins import PluginMount
|
from .plugins import PluginMount
|
||||||
from .utilities import axes_to_array
|
from .utilities import axes_to_array
|
||||||
from .task import TaskOrchestrator
|
from .task import TaskOrchestrator
|
||||||
from .lock import CompositeLock
|
from .lock import CompositeLock
|
||||||
from .config import OpenflexureConfig
|
from .config import OpenflexureSettingsFile, settings_to_json
|
||||||
|
|
||||||
|
|
||||||
class Microscope(object):
|
class Microscope:
|
||||||
"""
|
"""
|
||||||
A basic microscope object.
|
A basic microscope object.
|
||||||
|
|
||||||
The camera and stage should already be initialised, and passed as arguments.
|
The camera and stage should already be initialised, and passed as arguments.
|
||||||
|
|
||||||
Args:
|
|
||||||
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
|
|
||||||
stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
|
|
||||||
config (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object,
|
|
||||||
automatically created if None.
|
|
||||||
attach_plugins (bool): Should the microscope attach plugins listen in 'config' immediately.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
rc (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object,
|
settings_file (:py:class:`openflexure_microscope.config.OpenflexureSettingsFile`): Runtime-config object,
|
||||||
automatically created if None.
|
automatically created if None.
|
||||||
lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access
|
lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access
|
||||||
to multiple pieces of hardware.
|
to multiple pieces of hardware.
|
||||||
|
|
@ -40,43 +34,30 @@ class Microscope(object):
|
||||||
background tasks using microscope hardware
|
background tasks using microscope hardware
|
||||||
plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins
|
plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins
|
||||||
"""
|
"""
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
camera: StreamingCamera,
|
|
||||||
stage: OpenFlexureStage,
|
|
||||||
config=None,
|
|
||||||
attach_plugins: bool = True):
|
|
||||||
|
|
||||||
# Create RC object
|
def __init__(self):
|
||||||
if config:
|
# Initial attributes
|
||||||
self.rc = config
|
self.id = uuid.uuid4().hex
|
||||||
else:
|
self.name = self.id
|
||||||
self.rc = OpenflexureConfig(expand=True)
|
self.fov = [0, 0]
|
||||||
|
self.plugin_maps = []
|
||||||
|
self.camera = None
|
||||||
|
self.stage = None
|
||||||
|
|
||||||
# Initialise with an empty composite lock
|
# Initialise with an empty composite lock
|
||||||
self.lock = CompositeLock([])
|
self.lock = CompositeLock([])
|
||||||
|
|
||||||
# Attach initial hardware (may be NoneTypes)
|
|
||||||
self.camera = None
|
|
||||||
self.stage = None
|
|
||||||
self.attach(camera, stage)
|
|
||||||
|
|
||||||
# Create a task orchestrator
|
# Create a task orchestrator
|
||||||
self.task = TaskOrchestrator()
|
self.task = TaskOrchestrator()
|
||||||
|
|
||||||
# Create plugin mount-point
|
# Attach to a settings file
|
||||||
|
self.settings_file = OpenflexureSettingsFile(expand=True)
|
||||||
|
# Apply settings loaded from file
|
||||||
|
self.apply_config(self.settings_file.load())
|
||||||
|
|
||||||
|
# Create plugin mount-point and attach plugins from maps
|
||||||
self.plugin = PluginMount(self)
|
self.plugin = PluginMount(self)
|
||||||
|
self.attach_plugins(self.plugin_maps)
|
||||||
# Attach plugins
|
|
||||||
if attach_plugins:
|
|
||||||
self.attach_plugins()
|
|
||||||
|
|
||||||
# Set default parameters
|
|
||||||
if 'name' not in self.rc.config:
|
|
||||||
self.rc.write({'name': uuid.uuid4().hex})
|
|
||||||
|
|
||||||
if 'fov' not in self.rc.config:
|
|
||||||
self.rc.write({'fov': [0, 0]})
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Create microscope on context enter."""
|
"""Create microscope on context enter."""
|
||||||
|
|
@ -107,54 +88,47 @@ class Microscope(object):
|
||||||
stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
|
stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
settings_full = self.read_config()
|
||||||
|
|
||||||
|
# TODO: Actually attach dummy hardware!
|
||||||
|
# Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists
|
||||||
|
|
||||||
logging.debug("Attaching camera...")
|
logging.debug("Attaching camera...")
|
||||||
self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object
|
self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object
|
||||||
if isinstance(self.camera, StreamingCamera):
|
if not self.camera:
|
||||||
|
logging.info("No camera attached.")
|
||||||
|
else:
|
||||||
logging.info("Attached camera {}".format(camera))
|
logging.info("Attached camera {}".format(camera))
|
||||||
logging.info("Updating camera settings")
|
|
||||||
self.write_config(self.camera.read_config())
|
if hasattr(self.camera, 'lock'): # If camera has a lock
|
||||||
elif not self.camera:
|
logging.info("Attaching {} to composite lock.".format(self.camera.lock))
|
||||||
logging.info("Attached dummy camera.")
|
# Add the lock to the microscope composite lock
|
||||||
# If camera has a lock
|
self.lock.locks.append(self.camera.lock)
|
||||||
if hasattr(self.camera, '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...")
|
logging.debug("Attaching stage...")
|
||||||
self.stage = stage #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object
|
self.stage = stage #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object
|
||||||
if isinstance(self.stage, OpenFlexureStage): # If a stage object has been attached
|
if not self.stage:
|
||||||
|
logging.info("No stage attached.")
|
||||||
|
else:
|
||||||
logging.info("Attached stage {}".format(stage))
|
logging.info("Attached stage {}".format(stage))
|
||||||
self.stage.backlash = np.zeros(3, dtype=np.int)
|
|
||||||
elif not self.stage:
|
|
||||||
logging.info("Attached dummy stage.")
|
|
||||||
# If stage object has a lock
|
|
||||||
if hasattr(self.stage, 'lock'):
|
|
||||||
logging.info("Attaching lock {} to composite lock.".format(self.stage.lock))
|
|
||||||
# Add the lock to the microscope composite lock
|
|
||||||
self.lock.locks.append(self.stage.lock)
|
|
||||||
|
|
||||||
def attach_plugin_maps(self, plugin_maps):
|
if hasattr(self.stage, 'lock'): # If stage object has a lock
|
||||||
"""
|
logging.info("Attaching lock {} to composite lock.".format(self.stage.lock))
|
||||||
Attach plugins from a list of plugin map strings
|
# Add the lock to the microscope composite lock
|
||||||
|
self.lock.locks.append(self.stage.lock)
|
||||||
|
|
||||||
Args:
|
logging.info("Reapplying settings to newly attached devices")
|
||||||
plugin_maps (list): List of strings of plugin maps.
|
self.apply_config(settings_full)
|
||||||
"""
|
|
||||||
logging.debug("Attaching plugins...")
|
|
||||||
|
|
||||||
for plugin_map in plugin_maps:
|
def attach_plugins(self, plugin_maps: list):
|
||||||
self.plugin.attach(plugin_map)
|
|
||||||
logging.debug("Plugins attached.")
|
|
||||||
|
|
||||||
def attach_plugins(self):
|
|
||||||
"""
|
"""
|
||||||
Automatically search for plugin maps in self.config, and attach.
|
Automatically search for plugin maps in self.config, and attach.
|
||||||
"""
|
"""
|
||||||
if 'plugins' in self.rc.config:
|
if plugin_maps:
|
||||||
self.attach_plugin_maps(self.rc.config['plugins'])
|
for plugin_map in plugin_maps:
|
||||||
|
self.plugin.attach(plugin_map)
|
||||||
else:
|
else:
|
||||||
logging.warning("No plugins specified in microscope_settings.yaml. Skipping.")
|
logging.warning("No plugins specified. Skipping.")
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -163,7 +137,7 @@ class Microscope(object):
|
||||||
logging.info("Tearing down existing PluginMount...")
|
logging.info("Tearing down existing PluginMount...")
|
||||||
self.plugin = PluginMount(self)
|
self.plugin = PluginMount(self)
|
||||||
logging.info("Repopulating PluginMount...")
|
logging.info("Repopulating PluginMount...")
|
||||||
self.attach_plugins()
|
self.attach_plugins(self.plugin_maps)
|
||||||
|
|
||||||
# Create unified state
|
# Create unified state
|
||||||
@property
|
@property
|
||||||
|
|
@ -177,68 +151,92 @@ class Microscope(object):
|
||||||
state = {
|
state = {
|
||||||
'camera': self.camera.state,
|
'camera': self.camera.state,
|
||||||
'stage': self.stage.state,
|
'stage': self.stage.state,
|
||||||
'plugin': self.plugin.state
|
'plugin': self.plugin.state,
|
||||||
|
'version': pkg_resources.get_distribution('openflexure_microscope').version
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def write_config(self, config: dict):
|
def apply_config(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Writes and applies a config dictionary. Missing parameters will be left untouched.
|
Applies a config dictionary. Missing parameters will be left untouched.
|
||||||
"""
|
"""
|
||||||
|
logging.debug("Microscope: Applying config: {}".format(config))
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if self.camera:
|
if ('camera_settings' in config) and self.camera:
|
||||||
# Update camera config
|
# Update camera config
|
||||||
self.camera.apply_config(config)
|
self.camera.apply_config(config['camera_settings'])
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
# TODO: Convert stage settings into a config expansion
|
if ('stage_settings' in config) and self.stage:
|
||||||
if self.stage:
|
|
||||||
if 'backlash' in config:
|
if 'backlash' in config:
|
||||||
# Construct backlash array
|
# Construct backlash array
|
||||||
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
|
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
|
||||||
self.stage.backlash = backlash
|
self.stage.backlash = backlash
|
||||||
|
|
||||||
# Cache config
|
# Todo: tidy up with some loopy goodness
|
||||||
self.rc.write(config)
|
if 'id' in config:
|
||||||
|
self.id = config['id']
|
||||||
|
if 'name' in config:
|
||||||
|
self.name = config['name']
|
||||||
|
if 'fov' in config:
|
||||||
|
self.fov = config['fov']
|
||||||
|
if 'plugins' in config:
|
||||||
|
self.plugin_maps = config['plugins']
|
||||||
|
|
||||||
def read_config(self, json_safe=False):
|
def read_config(self, json_safe=False):
|
||||||
"""
|
"""
|
||||||
Read an updated config including camera settings.
|
Get an updated settings dictionary.
|
||||||
|
|
||||||
|
Reads current attributes and properties from connected hardware,
|
||||||
|
then merges those with the currently saved settings.
|
||||||
|
|
||||||
|
This is to ensure that settings for currently disconnected hardware
|
||||||
|
don't get removed from the settings file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
settings_current = {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'fov': self.fov,
|
||||||
|
'plugins': self.plugin_maps
|
||||||
|
}
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if self.camera:
|
if self.camera:
|
||||||
# Update camera config params from StreamingCamera object
|
settings_current_camera = self.camera.read_config()
|
||||||
self.rc.write(self.camera.read_config())
|
settings_current['camera_settings'] = settings_current_camera
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
|
# TODO: Offload to stage object
|
||||||
if self.stage:
|
if self.stage:
|
||||||
if hasattr(self.stage.backlash, 'tolist'):
|
if hasattr(self.stage.backlash, 'tolist'):
|
||||||
backlash = self.stage.backlash.tolist()
|
backlash = self.stage.backlash.tolist()
|
||||||
else:
|
else:
|
||||||
backlash = self.stage.backlash
|
backlash = self.stage.backlash
|
||||||
else:
|
|
||||||
backlash = [0, 0, 0]
|
|
||||||
|
|
||||||
backlash = {
|
settings_current_stage = {
|
||||||
'backlash': {
|
'backlash': {
|
||||||
'x': backlash[0],
|
'x': backlash[0],
|
||||||
'y': backlash[1],
|
'y': backlash[1],
|
||||||
'z': backlash[2],
|
'z': backlash[2],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.rc.write(backlash)
|
settings_current['stage_settings'] = settings_current_stage
|
||||||
|
|
||||||
return self.rc.read(json_safe=json_safe)
|
settings_full = self.settings_file.merge(settings_current)
|
||||||
|
|
||||||
|
if json_safe:
|
||||||
|
settings_full = settings_to_json(settings_full, clean_keys=True)
|
||||||
|
|
||||||
|
return settings_full
|
||||||
|
|
||||||
def save_config(self, backup: bool = True):
|
def save_config(self, backup: bool = True):
|
||||||
"""
|
"""
|
||||||
Saves the current runtime-config back to disk
|
Merges the current settings back to disk
|
||||||
"""
|
"""
|
||||||
# Get any changed device settings
|
self.settings_file.save(self.read_config(), backup=True)
|
||||||
self.read_config(json_safe=False)
|
|
||||||
# Save to disk
|
|
||||||
self.rc.save(backup=backup)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> dict:
|
def config(self) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ fov: [4100, 3146]
|
||||||
# Capture quality
|
# Capture quality
|
||||||
jpeg_quality: 75
|
jpeg_quality: 75
|
||||||
|
|
||||||
# Parameters specific to PiCamera objects
|
# Parameters specific to camera objects
|
||||||
picamera_settings: ~/.openflexure/picamera_settings.yaml
|
camera_settings: ~/.openflexure/camera_settings.yaml
|
||||||
|
|
||||||
|
# Parameters specific to stage objects
|
||||||
|
stage_settings: ~/.openflexure/stage_settings.yaml
|
||||||
|
|
||||||
# Default plugins
|
# Default plugins
|
||||||
plugins:
|
plugins:
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,10 @@ class AutofocusPlugin(MicroscopePlugin):
|
||||||
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
|
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
|
||||||
may cause you to overshoot, which is a problem.
|
may cause you to overshoot, which is a problem.
|
||||||
"""
|
"""
|
||||||
with self.monitor_sharpness() as m:
|
with self.monitor_sharpness() as m, self.microscope.camera.lock:
|
||||||
|
# Ensure the MJPEG stream has started
|
||||||
|
self.microscope.camera.start_stream_recording()
|
||||||
|
|
||||||
df = dz #TODO: refactor so I actually use dz in the code below!
|
df = dz #TODO: refactor so I actually use dz in the code below!
|
||||||
if initial_move_up:
|
if initial_move_up:
|
||||||
m.focus_rel(df/2)
|
m.focus_rel(df/2)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "openflexure_microscope"
|
name = "openflexure_microscope"
|
||||||
version = "1.0.2"
|
version = "1.1.0-beta.1"
|
||||||
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
|
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue