Commit 3925ef8c authored by Joel Collins's avatar Joel Collins
Browse files

Moved hardware config to a runtime config YAML file in home

parent 7a58b4e9
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
__all__ = ['Microscope']
__all__ = ['Microscope', 'config']
__version__ = "0.1.0"

from .microscope import Microscope
from . import config
 No newline at end of file
+0 −39
Original line number Diff line number Diff line
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)
 No newline at end of file
+0 −35
Original line number Diff line number Diff line
# 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
 No newline at end of file
+73 −63
Original line number Diff line number Diff line
@@ -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):
        """
        Open config_picamera.yaml file and write to camera.

        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`
    # 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.
        """
        global DEFAULT_CONFIG_PATH
        if not config_path:
            config_path = DEFAULT_CONFIG_PATH
        return hasattr(self.camera, "lens_shading_table")

        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'])

            # 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
            # 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]

            # 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 −0
Original line number Diff line number Diff line
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)
Loading