Moved hardware config to a runtime config YAML file in home
This commit is contained in:
parent
7a58b4e95e
commit
3925ef8cfe
10 changed files with 182 additions and 146 deletions
|
|
@ -1,4 +1,5 @@
|
|||
__all__ = ['Microscope']
|
||||
__all__ = ['Microscope', 'config']
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .microscope import Microscope
|
||||
from . import config
|
||||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
71
openflexure_microscope/config.py
Normal file
71
openflexure_microscope/config.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
20
openflexure_microscope/openflexurerc.default.yaml
Normal file
20
openflexure_microscope/openflexurerc.default.yaml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue