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" __version__ = "0.1.0"
from .microscope import Microscope from .microscope import Microscope
from . import config

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 from .base import BaseCamera, StreamObject
# 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
# 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): class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera.""" """Raspberry Pi camera implementation of StreamingCamera.
def __init__(self):
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 # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Attach to Pi camera # Attach to Pi camera
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object 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 # Default camera config
self.config.update({ self.config.update({
'video_resolution': (832, 624), 'video_resolution': (832, 624),
@ -70,13 +76,11 @@ class StreamingCamera(BaseCamera):
'jpeg_quality': 75, 'jpeg_quality': 75,
}) })
# Store state of StreamingCamera # Load config dictionary if passed
self.state.update({ if config:
'stream_active': False, self.update_settings(config)
'record_active': False,
'preview_active': False,
})
# Start streaming
self.start_worker() self.start_worker()
def initialisation(self): def initialisation(self):
@ -91,74 +95,82 @@ class StreamingCamera(BaseCamera):
if self.camera: if self.camera:
self.camera.close() self.camera.close()
# HANDLE SETTINGS # 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: def update_lens_shading(self, shading_array: np.ndarray):
config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH` if not self.supports_lens_shading:
""" logging.error("the currently-installed picamera library does not support all "
global DEFAULT_CONFIG_PATH "the features of the openflexure microscope software. These features "
if not config_path: "include lens shading control and setting the analog/digital gain.\n"
config_data = load_config(DEFAULT_CONFIG_PATH) "\n"
else: "See the installation instructions for how to fix this:\n"
config_data = load_config(config_path) "https://github.com/rwb27/openflexure_microscope_software")
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_settings(self, config: dict) -> None: def update_settings(self, config: dict) -> None:
""" """
Write a config dictionary to the StreamingCamera config. 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: Args:
config (dict): Dictionary of config parameters. 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 paused_stream = False
# 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
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.pause_stream_for_capture() # Pause stream self.pause_stream_for_capture() # Pause stream
paused_stream = True # Remember to unpause stream when done paused_stream = True # Remember to unpause stream when done
# Camera AWB # PiCamera parameters (applied directly to PiCamera object)
if 'awb_mode' in config: picamera_keys = [
self.camera.awb_mode = config['awb_mode'] 'exposure_mode',
if 'red_gain' in config and 'blue_gain' in config: 'awb_mode',
self.camera.awb_gains = ( 'awb_gains',
config['red_gain'], 'framerate',
config['blue_gain']) '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 # Richard's library to set analog and digital gains
if 'analog_gain' in config: if 'analog_gain' in config:
set_analog_gain(self.camera, config['analog_gain']) set_analog_gain(self.camera, config['analog_gain'])
@ -464,8 +476,6 @@ class StreamingCamera(BaseCamera):
self.wait_for_camera() self.wait_for_camera()
# Load config file
self.load_config()
# Set stream resolution # Set stream resolution
self.camera.resolution = self.config['video_resolution'] self.camera.resolution = self.config['video_resolution']
@ -490,7 +500,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.config['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()

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. Defines a microscope object, binding a camera and stage with basic functionality.
""" """
import logging import logging
import os
import numpy as np import numpy as np
from openflexure_stage import OpenFlexureStage from openflexure_stage import OpenFlexureStage
from .camera.pi import StreamingCamera 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): class Microscope(object):
@ -20,8 +22,11 @@ class Microscope(object):
Args: Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
microscope (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage 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): def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage):
self.attach(camera, stage) self.attach(camera, stage)
# Create plugin mountpoint # Create plugin mountpoint
@ -40,7 +45,7 @@ class Microscope(object):
self.camera.close() self.camera.close()
self.stage.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. Retroactively attaches a camera and stage to the microscope object.
@ -50,6 +55,7 @@ class Microscope(object):
Args: Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
microscope (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage 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 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. plugin_paths (list): List of strings of plugin directories.
include_default (bool): Also load plugins from the module default directory (DEFAULT_PLUGIN_PATH) 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: for i in plugins_list:
module = load_plugin(i) 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

View file

@ -1,7 +1,7 @@
-e git://github.com/rwb27/openflexure_nano_motor_controller.git#egg=openflexure_stage -e git://github.com/rwb27/openflexure_nano_motor_controller.git#egg=openflexure_stage
-e git://github.com/rwb27/picamera.git#egg=picamera
flask >= 0.12.3 flask >= 0.12.3
picamera==1.13
pyyaml==3.13 pyyaml==3.13
numpy numpy
Pillow Pillow

View file

@ -1,15 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_stage import OpenFlexureStage from openflexure_stage import OpenFlexureStage
from openflexure_microscope import Microscope from openflexure_microscope import Microscope, config
from openflexure_microscope.plugins import PluginMount, load_plugin, search_plugin_paths
import atexit import atexit
import logging, sys import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
if __name__ == '__main__': if __name__ == '__main__':
microscope = Microscope(StreamingCamera(), OpenFlexureStage("/dev/ttyUSB0")) openflexurerc = config.load_config()
microscope = Microscope(StreamingCamera(openflexurerc), OpenFlexureStage("/dev/ttyUSB0"))
microscope.find_plugins() # Automatically find microscope plugins microscope.find_plugins() # Automatically find microscope plugins