Moved hardware config to a runtime config YAML file in home

This commit is contained in:
Joel Collins 2018-11-29 14:41:56 +00:00
parent 7a58b4e95e
commit 3925ef8cfe
10 changed files with 182 additions and 146 deletions

View file

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

View file

@ -261,7 +261,7 @@ class PositionAPI(MicroscopeView):
return jsonify(self.microscope.state['position'])
app.add_url_rule(
uri('/position/'),
uri('/position/'),
view_func=PositionAPI.as_view('position', microscope=api_microscope))

View file

@ -1,39 +0,0 @@
import yaml
TYPES = {
'stream_resolution': tuple,
'video_resolution': tuple,
'image_resolution': tuple,
'numpy_resolution': tuple,
'jpeg_quality': int,
'framerate': int,
'awb_mode': str,
'red_gain': float,
'blue_gain': float,
'shutter_speed': int,
'saturation': int,
'analog_gain': float,
'digital_gain': float,
}
def convert_config(config: dict) -> dict:
"""Convert datatype of config based on type dictionary."""
global TYPES
for key in config:
if key in TYPES:
config[key] = TYPES[key](config[key])
return config
def load_config(yaml_path: str) -> dict:
"""Load YAML file, pass through dictionary conversion, and return."""
with open(yaml_path) as config_file:
return yaml.load(config_file)
def save_config(config: dict, yaml_path: str):
with open('yaml_path', 'w') as outfile:
yaml.dump(config, outfile)

View file

@ -1,35 +0,0 @@
# Exposure mode
exposure_mode: 'off'
# The analog gain offers higher sensitivity and less noise than using digital gain only
analog_gain: 1.
digital_gain: 1.
# When queried, the shutter_speed property returns the shutter speed of the camera in microseconds,
# or 0 which indicates that the speed will be automatically determined.
# If high value does not work, need to decrease the fps for streaming
shutter_speed: 30000
# Auto white balance: The red and blue values are returned Fraction instances.
# The values will be between 0.0 and 8.0.
awb_mode: 'off'
# If not using auto, then you can change the awb_gains
red_gain: 1.3
blue_gain: 1.2
# Color saturation of the camera as an integer between -100 and 100.
saturation: 0
# ISO Valid values are between 0 (auto) and 800 (1600 is not available?).
# ISO value is not used anyway as we are fixing the gains
iso: 500
# Resolutions for streaming and capture
video_resolution: [832, 624]
image_resolution: [2592, 1944]
numpy_resolution: [1312, 976]
# Capture quality
jpeg_quality: 75
framerate: 24

View file

@ -47,21 +47,27 @@ import threading
from .base import BaseCamera, StreamObject
# Richard's fix gain
from .set_picamera_gain import set_analog_gain, set_digital_gain
# Manage config
from .config import load_config, save_config, convert_config
HERE = os.path.abspath(os.path.dirname(__file__))
DEFAULT_CONFIG_PATH = os.path.join(HERE, 'config_picamera.yaml')
class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera."""
def __init__(self):
"""Raspberry Pi camera implementation of StreamingCamera.
Args:
config (dict): Dictionary of config parameters to apply on init. If None, will default to basic config.
"""
def __init__(self, config: dict=None):
# Run BaseCamera init
BaseCamera.__init__(self)
# Attach to Pi camera
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object
# Store state of StreamingCamera
self.state.update({
'stream_active': False,
'record_active': False,
'preview_active': False,
})
# Default camera config
self.config.update({
'video_resolution': (832, 624),
@ -70,13 +76,11 @@ class StreamingCamera(BaseCamera):
'jpeg_quality': 75,
})
# Store state of StreamingCamera
self.state.update({
'stream_active': False,
'record_active': False,
'preview_active': False,
})
# Load config dictionary if passed
if config:
self.update_settings(config)
# Start streaming
self.start_worker()
def initialisation(self):
@ -91,74 +95,82 @@ class StreamingCamera(BaseCamera):
if self.camera:
self.camera.close()
# HANDLE SETTINGS
def load_config(self, config_path: str=None):
@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.
"""
Open config_picamera.yaml file and write to camera.
return hasattr(self.camera, "lens_shading_table")
Args:
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
"""
global DEFAULT_CONFIG_PATH
if not config_path:
config_data = load_config(DEFAULT_CONFIG_PATH)
else:
config_data = load_config(config_path)
self.update_settings(config_data)
def save_config(self, config_path: str=None):
"""
Save current config dictionary to a YAML file.
Args:
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
"""
global DEFAULT_CONFIG_PATH
if not config_path:
config_path = DEFAULT_CONFIG_PATH
save_config(self.config, config_path)
def update_lens_shading(self, shading_array: np.ndarray):
if not self.supports_lens_shading:
logging.error("the currently-installed picamera library does not support all "
"the features of the openflexure microscope software. These features "
"include lens shading control and setting the analog/digital gain.\n"
"\n"
"See the installation instructions for how to fix this:\n"
"https://github.com/rwb27/openflexure_microscope_software")
def update_settings(self, config: dict) -> None:
"""
Write a config dictionary to the StreamingCamera config.
The passed dictionary may contain other parameters not relevant to
camera config. Eg. Passing a general config file will work fine.
Args:
config (dict): Dictionary of config parameters.
"""
self.config = convert_config(config) # Convert loaded dictionary to valid config dictionary
logging.debug(self.config)
paused_stream = False
# Apply valid config params to Picamera object
if not self.state['record_active']: # If not recording a video
logging.debug("Applying config:")
logging.debug(config)
# Pause stream while changing settings
if self.state['stream_active']: # If stream is active
logging.info("Pausing stream to update config.")
self.pause_stream_for_capture() # Pause stream
paused_stream = True # Remember to unpause stream when done
# Camera AWB
if 'awb_mode' in config:
self.camera.awb_mode = config['awb_mode']
if 'red_gain' in config and 'blue_gain' in config:
self.camera.awb_gains = (
config['red_gain'],
config['blue_gain'])
# PiCamera parameters (applied directly to PiCamera object)
picamera_keys = [
'exposure_mode',
'awb_mode',
'awb_gains',
'framerate',
'shutter_speed',
'saturation',
'led'
]
if 'picamera_params' in config:
for key in picamera_keys:
if key in config['picamera_params']:
logging.debug("Setting parameter {}: {}".format(key, config['picamera_params'][key]))
setattr(self.camera, key, config['picamera_params'][key])
# StreamingCamera parameters (applied via StreamingCamera config)
config_keys = [
'video_resolution',
'image_resolution',
'numpy_resolution',
'jpeg_quality', ]
for key in config_keys:
if key in config:
logging.debug("Setting parameter {}: {}".format(key, config[key]))
self.config[key] = config[key]
# Camera framerate
if 'framerate' in config:
self.camera.framerate = config['framerate']
# Camera exposure
if 'shutter_speed' in config:
self.camera.shutter_speed = config['shutter_speed']
if 'saturation' in config:
self.camera.saturation = config['saturation']
# Camera misc.
self.camera.led = False
# Richard's library to set analog and digital gains
if 'analog_gain' in config:
set_analog_gain(self.camera, config['analog_gain'])
@ -464,8 +476,6 @@ class StreamingCamera(BaseCamera):
self.wait_for_camera()
# Load config file
self.load_config()
# Set stream resolution
self.camera.resolution = self.config['video_resolution']
@ -490,7 +500,7 @@ class StreamingCamera(BaseCamera):
self.stream.seek(0)
self.stream.truncate()
# to stream, read the new frame
time.sleep(1/self.config['framerate']*0.1)
time.sleep(1/self.camera.framerate*0.1)
# yield the result to be read
frame = self.stream.getvalue()

View file

@ -0,0 +1,71 @@
import yaml
import os
import logging
import shutil
HERE = os.path.abspath(os.path.dirname(__file__))
DEFAULT_CONFIG_PATH = os.path.join(HERE, 'openflexurerc.default.yaml')
USER_CONFIG_PATH = os.path.join(os.path.expanduser("~"), "openflexurerc.yaml")
TYPES = {
'stream_resolution': tuple,
'video_resolution': tuple,
'image_resolution': tuple,
'numpy_resolution': tuple,
'jpeg_quality': int,
'analog_gain': float,
'digital_gain': float,
}
def convert_config(config: dict) -> dict:
"""Convert datatype of config based on type dictionary."""
global TYPES
for key in config:
if key in TYPES:
config[key] = TYPES[key](config[key])
return config
def load_config(config_path: str=None) -> dict:
"""
Open openflexurerc.yaml file and write to the microscope devices.
Args:
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
"""
global DEFAULT_CONFIG_PATH, USER_CONFIG_PATH
if not config_path:
if os.path.exists(USER_CONFIG_PATH): # If user config file already exists
config_path = USER_CONFIG_PATH # Load it
else: # If user config file doesn't yet exist
logging.warning("No user config found. Loading system defaults...")
logging.info("Copying default config to user config...")
shutil.copyfile(DEFAULT_CONFIG_PATH, USER_CONFIG_PATH)
logging.info("Loading user config...")
config_path = USER_CONFIG_PATH # Load defaults in
with open(config_path) as config_file:
config_data = yaml.load(config_file)
# Store config dictionary to self
return convert_config(config_data)
def save_config(self, config_dict: dict, config_path: str=None):
"""
Save current config dictionary to a YAML file.
Args:
config_dict (dict): Dictionary of config data to save.
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH`
"""
global USER_CONFIG_PATH
if not config_path:
config_path = USER_CONFIG_PATH
with open(config_path, 'w') as outfile:
yaml.dump(config_dict, outfile)

View file

@ -3,12 +3,14 @@
Defines a microscope object, binding a camera and stage with basic functionality.
"""
import logging
import os
import numpy as np
from openflexure_stage import OpenFlexureStage
from .camera.pi import StreamingCamera
from .plugins import PluginMount, load_plugin, search_plugin_paths
from .plugins import PluginMount, load_plugin, search_plugin_dirs
from .config import load_config, save_config, convert_config
class Microscope(object):
@ -20,8 +22,11 @@ class Microscope(object):
Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
microscope (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
load_config (bool): Should a config file be automatically loaded on init?
config_path (str): Path to the config YAML file to be loaded. If None, defaults to USER_CONFIG_PATH.
"""
def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage):
self.attach(camera, stage)
# Create plugin mountpoint
@ -40,7 +45,7 @@ class Microscope(object):
self.camera.close()
self.stage.close()
def attach(self, camera: StreamingCamera, stage: OpenFlexureStage):
def attach(self, camera: StreamingCamera, stage: OpenFlexureStage, apply_config: bool=True):
"""
Retroactively attaches a camera and stage to the microscope object.
@ -50,6 +55,7 @@ class Microscope(object):
Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
microscope (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
apply_config (bool): Should the microscope config dictionary be applied to the attached hardware?
"""
self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object
@ -69,7 +75,8 @@ class Microscope(object):
plugin_paths (list): List of strings of plugin directories.
include_default (bool): Also load plugins from the module default directory (DEFAULT_PLUGIN_PATH)
"""
plugins_list = search_plugin_paths(plugin_paths, include_default=include_default)
# TODO: Get paths from rc file
plugins_list = search_plugin_dirs(plugin_paths, include_default=include_default)
for i in plugins_list:
module = load_plugin(i)

View file

@ -0,0 +1,20 @@
# The analog gain offers higher sensitivity and less noise than using digital gain only
analog_gain: 1.
digital_gain: 1.
# Resolutions for streaming and capture
video_resolution: [832, 624]
image_resolution: [2592, 1944]
numpy_resolution: [1312, 976]
# Capture quality
jpeg_quality: 75
picamera_params:
exposure_mode: 'off'
awb_mode: 'off'
awb_gains: [1.3, 1.2]
framerate: 24
shutter_speed: 30000
saturation: 0
led: false