Introduced apply_config and read_config methods

This commit is contained in:
Joel Collins 2019-01-31 13:29:07 +00:00
parent e5c10adf8d
commit 5471ffa62b
2 changed files with 93 additions and 91 deletions

View file

@ -25,13 +25,7 @@ from __future__ import division
import io import io
import time import time
import datetime
import sys
import os
import yaml
import copy
import numpy as np import numpy as np
from PIL import Image
import logging import logging
# Used for conversion only # Used for conversion only
@ -44,9 +38,6 @@ import picamera.array
# Type hinting # Type hinting
from typing import Tuple from typing import Tuple
# Threading
import threading
from .base import BaseCamera, CaptureObject 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
@ -70,9 +61,9 @@ CONFIG_KEYS = {
}, },
} }
# Useful methods # Useful methods
# TODO: Remove this
def fractions_to_floats(value): def fractions_to_floats(value):
"""Deal with horrible, horrible PiCamera fractions""" """Deal with horrible, horrible PiCamera fractions"""
result = value result = value
@ -85,15 +76,15 @@ def fractions_to_floats(value):
result = float(value) result = float(value)
return result return result
# MAIN CLASS
# MAIN CLASS
class StreamingCamera(BaseCamera): class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera. """Raspberry Pi camera implementation of StreamingCamera.
Args: Args:
config (dict): Dictionary of config parameters to apply on init. If None, will default to basic config. config (dict): Dictionary of config parameters to apply on init. If None, will default to basic config.
""" """
def __init__(self, config: dict=None, picamera_settings: dict=None): def __init__(self, config: dict = None):
global CONFIG_KEYS global CONFIG_KEYS
# Run BaseCamera init # Run BaseCamera init
@ -111,11 +102,14 @@ class StreamingCamera(BaseCamera):
self.set_zoom(1.0) self.set_zoom(1.0)
# Populate config and settings with all available keys # Populate config and settings with all available keys
self._config.update(CONFIG_KEYS) self.config.update(CONFIG_KEYS)
# Load config dictionary if passed # Load config dictionary if passed
if config: if config:
self.config = config self.apply_config(config)
# Create an empty stream
self.stream = io.BytesIO()
# Start streaming # Start streaming
self.start_worker() self.start_worker()
@ -128,7 +122,7 @@ class StreamingCamera(BaseCamera):
"""Close the Raspberry Pi StreamingCamera.""" """Close the Raspberry Pi StreamingCamera."""
# Run BaseCamera close method # Run BaseCamera close method
BaseCamera.close(self) BaseCamera.close(self)
# Detatch Pi camera # Detach Pi camera
if self.camera: if self.camera:
self.camera.close() self.camera.close()
@ -146,8 +140,7 @@ class StreamingCamera(BaseCamera):
""" """
return hasattr(self.camera, "lens_shading_table") return hasattr(self.camera, "lens_shading_table")
@property def read_config(self):
def config(self):
""" """
Return config dictionary of the StreamingCamera. Return config dictionary of the StreamingCamera.
""" """
@ -156,7 +149,7 @@ class StreamingCamera(BaseCamera):
} }
# PiCamera parameters (obtained directly from PiCamera object) # PiCamera parameters (obtained directly from PiCamera object)
for key, _ in self._config['picamera_settings'].items(): for key, _ in self.config['picamera_settings'].items():
try: try:
value = getattr(self.camera, key) value = getattr(self.camera, key)
value = fractions_to_floats(value) value = fractions_to_floats(value)
@ -166,13 +159,12 @@ class StreamingCamera(BaseCamera):
# StreamingCamera parameters (obtained from StreamingCamera _config) # StreamingCamera parameters (obtained from StreamingCamera _config)
for key in CONFIG_KEYS: for key in CONFIG_KEYS:
if (key != 'picamera_settings') and (key in self._config): if (key != 'picamera_settings') and (key in self.config):
conf_dict[key] = self._config[key] conf_dict[key] = self.config[key]
return conf_dict return conf_dict
@config.setter def apply_config(self, config: dict) -> None:
def config(self, config: dict) -> None:
""" """
Write a config dictionary to the StreamingCamera config. Write a config dictionary to the StreamingCamera config.
@ -202,7 +194,7 @@ class StreamingCamera(BaseCamera):
# PiCamera parameters (applied directly to PiCamera object) # PiCamera parameters (applied directly to PiCamera object)
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 for key, value in config['picamera_settings'].items(): # For each given setting
self._config['picamera_settings'][key] = None # Add the key to the list of returned settings 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])) logging.debug("Setting parameter {}: {}".format(key, config['picamera_settings'][key]))
# Handle special attributes: # Handle special attributes:
@ -217,7 +209,7 @@ class StreamingCamera(BaseCamera):
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': # We already handled this
logging.debug("Setting parameter {}: {}".format(key, value)) logging.debug("Setting parameter {}: {}".format(key, value))
self._config[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:
@ -228,9 +220,9 @@ class StreamingCamera(BaseCamera):
raise Exception( raise Exception(
"Cannot update camera config while recording is active.") "Cannot update camera config while recording is active.")
def set_zoom(self, zoom_value: float=1.) -> None: def set_zoom(self, zoom_value: float = 1.) -> None:
""" """
Change the camera zoom, handling recentering and scaling. Change the camera zoom, handling re-centering and scaling.
""" """
with self.lock: with self.lock:
self.state['zoom_value'] = float(zoom_value) self.state['zoom_value'] = float(zoom_value)
@ -253,13 +245,13 @@ class StreamingCamera(BaseCamera):
# LAUNCH ACTIONS # LAUNCH ACTIONS
def start_preview(self) -> bool: def start_preview(self) -> bool:
"""Start the onboard GPU camera preview.""" """Start the on board GPU camera preview."""
self.camera.start_preview() self.camera.start_preview()
self.state['preview_active'] = True self.state['preview_active'] = True
return True return True
def stop_preview(self) -> bool: def stop_preview(self) -> bool:
"""Stop the onboard 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
return True return True
@ -267,17 +259,16 @@ class StreamingCamera(BaseCamera):
def start_recording( def start_recording(
self, self,
output, output,
fmt: str='h264', fmt: str = 'h264',
quality: int=15): quality: int = 15):
"""Start recording. """Start recording.
Start a new video recording, writing to a output object. Start a new video recording, writing to a output object.
Args: Args:
output (CaptureObject/str): Output object to write data bytes to. output (CaptureObject/str): Output object to write data bytes to.
write_to_file (bool/NoneType): Should the StreamObject write to a file?
filename (str): Name of the stored file. Defaults to timestamp.
fmt (str): Format of the capture. fmt (str): Format of the capture.
quality (int): Video recording quality.
Returns: Returns:
output_object (str/BytesIO): Target object. output_object (str/BytesIO): Target object.
@ -317,7 +308,7 @@ class StreamingCamera(BaseCamera):
until the current recording has stopped.") until the current recording has stopped.")
return None return None
def stop_recording(self) -> bool: def stop_recording(self):
"""Stop the last started video recording on splitter port 2.""" """Stop the last started video recording on splitter port 2."""
with self.lock: with self.lock:
# Stop the camera video recording on port 2 # Stop the camera video recording on port 2
@ -330,8 +321,8 @@ class StreamingCamera(BaseCamera):
def stop_stream_recording( def stop_stream_recording(
self, self,
splitter_port: int=1, splitter_port: int = 1,
resolution: Tuple[int, int]=None) -> None: resolution: Tuple[int, int] = None) -> None:
""" """
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active. Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
@ -355,8 +346,8 @@ class StreamingCamera(BaseCamera):
def start_stream_recording( def start_stream_recording(
self, self,
splitter_port: int=1, splitter_port: int = 1,
resolution: Tuple[int, int]=None) -> None: resolution: Tuple[int, int] = None) -> None:
""" """
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active. Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
@ -364,9 +355,9 @@ class StreamingCamera(BaseCamera):
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. Defaults to `self.config['video_resolution']`. resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.config['video_resolution']`.
""" """
# If no stream object exists # If stream object was destroyed
if not hasattr(self, 'stream'): if not hasattr(self, 'stream'):
self.stream = io.BytesIO() # Create a stream object self.stream = io.BytesIO() # Create a stream object
# If no explicit resolution is passed # If no explicit resolution is passed
if not resolution: if not resolution:
@ -389,10 +380,10 @@ class StreamingCamera(BaseCamera):
def capture( def capture(
self, self,
output, output,
fmt: str='jpeg', fmt: str = 'jpeg',
use_video_port: bool=False, use_video_port: bool = False,
resize: Tuple[int, int]=None, resize: Tuple[int, int] = None,
bayer: bool=True): bayer: bool = True):
""" """
Capture a still image to a StreamObject. Capture a still image to a StreamObject.
@ -404,6 +395,7 @@ class StreamingCamera(BaseCamera):
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
fmt (str): Format of the capture. fmt (str): Format of the capture.
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
bayer (bool): Store raw bayer data in capture
""" """
with self.lock: with self.lock:
@ -416,7 +408,7 @@ class StreamingCamera(BaseCamera):
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
# Set resolution and stop stream recording if necesarry # Set resolution and stop stream recording if necessary
if not use_video_port: if not use_video_port:
self.stop_stream_recording() self.stop_stream_recording()
@ -428,7 +420,7 @@ class StreamingCamera(BaseCamera):
bayer=(not use_video_port) and bayer, bayer=(not use_video_port) and bayer,
use_video_port=use_video_port) use_video_port=use_video_port)
# Set resolution and start stream recording if necesarry # Set resolution and start stream recording if necessary
if not use_video_port: if not use_video_port:
self.start_stream_recording() self.start_stream_recording()
@ -436,9 +428,8 @@ class StreamingCamera(BaseCamera):
def yuv( def yuv(
self, self,
rgb=False, use_video_port: bool = True,
use_video_port: bool=True, resize: Tuple[int, int] = None) -> np.ndarray:
resize: Tuple[int, int]=None) -> np.ndarray:
"""Capture an uncompressed still YUV image to a Numpy array. """Capture an uncompressed still YUV image to a Numpy array.
Args: Args:
@ -473,16 +464,12 @@ class StreamingCamera(BaseCamera):
if not use_video_port: if not use_video_port:
self.start_stream_recording() self.start_stream_recording()
if rgb: return output.array
logging.debug("Converting to RGB")
return output.rgb_array
else:
return output.array
def array( def array(
self, self,
use_video_port: bool=True, use_video_port: bool = True,
resize: Tuple[int, int]=None) -> np.ndarray: resize: Tuple[int, int] = None) -> np.ndarray:
"""Capture an uncompressed still RGB image to a Numpy array. """Capture an uncompressed still RGB image to a Numpy array.
Args: Args:
@ -537,7 +524,6 @@ class StreamingCamera(BaseCamera):
# Update state # Update state
logging.debug("STREAM ACTIVE") logging.debug("STREAM ACTIVE")
#self.state['stream_active'] = True
# While the iterator is not closed # While the iterator is not closed
try: try:

View file

@ -25,22 +25,29 @@ 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 stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
config (dict): Dictionary of the runtime config to apply to the microscope. Does not automatically apply to camera and stage. config_path (str): Path to a microscoperc.yaml runtime-config file
attach_plugins (bool): Should the microscope attach plugins listen in 'config' immediately. attach_plugins (bool): Should the microscope attach plugins listen in 'config' immediately.
""" """
def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage, config_path: str=None, attach_plugins: bool=True): def __init__(
self,
camera: StreamingCamera,
stage: OpenFlexureStage,
config_path: str = None,
attach_plugins: bool = True):
# Create RC object # Create RC object
self.rc = OpenflexureConfig(config_path=config_path, expand=True) self.rc = OpenflexureConfig(config_path=config_path, expand=True)
# Attach initial hardware (may be NoneTypes) # Attach initial hardware (may be NoneTypes)
self.camera = None
self.stage = None
self.attach(camera, stage) self.attach(camera, stage)
# Create a task orchestrator # Create a task orchestrator
self.task = TaskOrchestrator() #: :py:class:`openflexure_microscope.task.TaskOrchestrator`: Threaded task orchestrator self.task = TaskOrchestrator() #: :py:class:`openflexure_microscope.task.TaskOrchestrator`: Threaded task orchestrator
# Create plugin mountpoint # Create plugin mount-point
self.plugin = PluginMount(self) #: :py:class:`openflexure_microscope.plugins.PluginMount`: Mounting point for all microscope plugins self.plugin = PluginMount(self) #: :py:class:`openflexure_microscope.plugins.PluginMount`: Mounting point for all microscope plugins
# Attach plugins # Attach plugins
@ -48,16 +55,15 @@ class Microscope(object):
self.attach_plugins() self.attach_plugins()
# Set default parameters # Set default parameters
if not 'name' in self.rc.config: if 'name' not in self.rc.config:
self.rc.config['name'] = uuid.uuid4().hex self.rc.update({'name': uuid.uuid4().hex})
if not 'fov' in self.rc.config: if 'fov' not in self.rc.config:
self.rc.config['fov'] = [0, 0] self.rc.update({'fov': [0, 0]})
# Initialise with an empty composite lock # Initialise with an empty composite lock
self.lock = CompositeLock([]) #: :py:class:`openflexure_microscope.lock.CompositeLock`: Composite lock controlling thread access to multiple pieces of hardware self.lock = CompositeLock([]) #: :py:class:`openflexure_microscope.lock.CompositeLock`: Composite lock controlling thread access to multiple pieces of hardware
def __enter__(self): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
return self return self
@ -82,14 +88,15 @@ 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 stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
apply_config (bool): Should the microscope config dictionary be applied to the attached hardware?
""" """
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 isinstance(self.camera, StreamingCamera):
logging.info("Attached camera {}".format(camera)) logging.info("Attached camera {}".format(camera))
logging.info("Updating camera settings")
self.apply_config(self.camera.read_config())
elif not self.camera: elif not self.camera:
logging.info("Attached dummy camera.") logging.info("Attached dummy camera.")
# If camera has a lock # If camera has a lock
@ -173,13 +180,37 @@ class Microscope(object):
return state return state
@property def apply_config(self, config: dict):
def config(self): """
Updates and applies a config dictionary. Missing parameters will be left untouched.
"""
# If attached to a camera
if self.camera:
# Update camera config
self.camera.config = config
# If attached to a stage
# TODO: Convert stage settings into a config expansion
if self.stage:
if 'backlash' in config:
# Construct backlash array
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
self.stage.backlash = backlash
# Cache config
self.rc.update(config)
# Save to disk
self.rc.save()
def read_config(self):
"""
Read an updated config including camera settings.
"""
# If attached to a camera # If attached to a camera
if self.camera: if self.camera:
# Update camera config params from StreamingCamera object # Update camera config params from StreamingCamera object
self.rc.update(self.camera.config) self.rc.update(self.camera.config)
# If attached to a stage # If attached to a stage
if self.stage: if self.stage:
backlash = self.stage.backlash.tolist() backlash = self.stage.backlash.tolist()
@ -196,27 +227,12 @@ class Microscope(object):
self.rc.update(backlash) self.rc.update(backlash)
return self.rc.config return self.rc.asdict()
@property
def config(self) -> dict:
return self.read_config()
@config.setter @config.setter
def config(self, config: dict) -> None: def config(self, config: dict) -> None:
""" self.apply_config(config)
Technically doesn't SET the config, but rather updates it. Missing parameters
will be left untouched.
"""
# If attached to a camera
if self.camera:
# Update camera config
self.camera.config = config
# If attached to a stage
if self.stage:
if 'backlash' in config:
# Construct backlash array
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
self.stage.backlash = backlash
# Cache config
self.rc.update(config)
# Save to disk
self.rc.save()