Loading openflexure_microscope/camera/pi.py +23 −37 Original line number Diff line number Diff line Loading @@ -25,13 +25,7 @@ from __future__ import division import io import time import datetime import sys import os import yaml import copy import numpy as np from PIL import Image import logging # Used for conversion only Loading @@ -44,9 +38,6 @@ import picamera.array # Type hinting from typing import Tuple # Threading import threading from .base import BaseCamera, CaptureObject # Richard's fix gain from .set_picamera_gain import set_analog_gain, set_digital_gain Loading @@ -70,9 +61,9 @@ CONFIG_KEYS = { }, } # Useful methods # TODO: Remove this def fractions_to_floats(value): """Deal with horrible, horrible PiCamera fractions""" result = value Loading @@ -85,15 +76,15 @@ def fractions_to_floats(value): result = float(value) return result # MAIN CLASS # MAIN CLASS class StreamingCamera(BaseCamera): """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, picamera_settings: dict=None): def __init__(self, config: dict = None): global CONFIG_KEYS # Run BaseCamera init Loading @@ -111,11 +102,14 @@ class StreamingCamera(BaseCamera): self.set_zoom(1.0) # Populate config and settings with all available keys self._config.update(CONFIG_KEYS) self.config.update(CONFIG_KEYS) # Load config dictionary if passed if config: self.config = config self.apply_config(config) # Create an empty stream self.stream = io.BytesIO() # Start streaming self.start_worker() Loading @@ -128,7 +122,7 @@ class StreamingCamera(BaseCamera): """Close the Raspberry Pi StreamingCamera.""" # Run BaseCamera close method BaseCamera.close(self) # Detatch Pi camera # Detach Pi camera if self.camera: self.camera.close() Loading @@ -146,8 +140,7 @@ class StreamingCamera(BaseCamera): """ return hasattr(self.camera, "lens_shading_table") @property def config(self): def read_config(self): """ Return config dictionary of the StreamingCamera. """ Loading @@ -156,7 +149,7 @@ class StreamingCamera(BaseCamera): } # PiCamera parameters (obtained directly from PiCamera object) for key, _ in self._config['picamera_settings'].items(): for key, _ in self.config['picamera_settings'].items(): try: value = getattr(self.camera, key) value = fractions_to_floats(value) Loading @@ -166,13 +159,12 @@ class StreamingCamera(BaseCamera): # StreamingCamera parameters (obtained from StreamingCamera _config) for key in CONFIG_KEYS: if (key != 'picamera_settings') and (key in self._config): conf_dict[key] = self._config[key] if (key != 'picamera_settings') and (key in self.config): conf_dict[key] = self.config[key] return conf_dict @config.setter def config(self, config: dict) -> None: def apply_config(self, config: dict) -> None: """ Write a config dictionary to the StreamingCamera config. Loading Loading @@ -202,7 +194,7 @@ class StreamingCamera(BaseCamera): # PiCamera parameters (applied directly to PiCamera object) if 'picamera_settings' in config: # If new settings are given 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])) # Handle special attributes: Loading @@ -217,7 +209,7 @@ class StreamingCamera(BaseCamera): for key, value in config.items(): # For each provided setting if key != 'picamera_settings': # We already handled this logging.debug("Setting parameter {}: {}".format(key, value)) self._config[key] = value self.config[key] = value # If stream was paused to update config, unpause if paused_stream: Loading @@ -230,7 +222,7 @@ class StreamingCamera(BaseCamera): 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: self.state['zoom_value'] = float(zoom_value) Loading Loading @@ -275,9 +267,8 @@ class StreamingCamera(BaseCamera): Args: 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. quality (int): Video recording quality. Returns: output_object (str/BytesIO): Target object. Loading Loading @@ -317,7 +308,7 @@ class StreamingCamera(BaseCamera): until the current recording has stopped.") return None def stop_recording(self) -> bool: def stop_recording(self): """Stop the last started video recording on splitter port 2.""" with self.lock: # Stop the camera video recording on port 2 Loading Loading @@ -364,7 +355,7 @@ class StreamingCamera(BaseCamera): 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']`. """ # If no stream object exists # If stream object was destroyed if not hasattr(self, 'stream'): self.stream = io.BytesIO() # Create a stream object Loading Loading @@ -404,6 +395,7 @@ class StreamingCamera(BaseCamera): use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. fmt (str): Format of the capture. resize ((int, int)): Resize the captured image. bayer (bool): Store raw bayer data in capture """ with self.lock: Loading @@ -416,7 +408,7 @@ class StreamingCamera(BaseCamera): 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: self.stop_stream_recording() Loading @@ -428,7 +420,7 @@ class StreamingCamera(BaseCamera): bayer=(not use_video_port) and bayer, 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: self.start_stream_recording() Loading @@ -436,7 +428,6 @@ class StreamingCamera(BaseCamera): def yuv( self, rgb=False, use_video_port: bool = True, resize: Tuple[int, int] = None) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Loading Loading @@ -473,10 +464,6 @@ class StreamingCamera(BaseCamera): if not use_video_port: self.start_stream_recording() if rgb: logging.debug("Converting to RGB") return output.rgb_array else: return output.array def array( Loading Loading @@ -537,7 +524,6 @@ class StreamingCamera(BaseCamera): # Update state logging.debug("STREAM ACTIVE") #self.state['stream_active'] = True # While the iterator is not closed try: Loading openflexure_microscope/microscope.py +50 −34 Original line number Diff line number Diff line Loading @@ -25,22 +25,29 @@ class Microscope(object): Args: camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object microscope (: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. stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object config_path (str): Path to a microscoperc.yaml runtime-config file 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 self.rc = OpenflexureConfig(config_path=config_path, expand=True) # Attach initial hardware (may be NoneTypes) self.camera = None self.stage = None self.attach(camera, stage) # Create a 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 # Attach plugins Loading @@ -48,16 +55,15 @@ class Microscope(object): self.attach_plugins() # Set default parameters if not 'name' in self.rc.config: self.rc.config['name'] = uuid.uuid4().hex if 'name' not in self.rc.config: self.rc.update({'name': uuid.uuid4().hex}) if not 'fov' in self.rc.config: self.rc.config['fov'] = [0, 0] if 'fov' not in self.rc.config: self.rc.update({'fov': [0, 0]}) # 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 def __enter__(self): """Create microscope on context enter.""" return self Loading @@ -82,14 +88,15 @@ 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? stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object """ logging.debug("Attaching camera...") self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object if isinstance(self.camera, StreamingCamera): logging.info("Attached camera {}".format(camera)) logging.info("Updating camera settings") self.apply_config(self.camera.read_config()) elif not self.camera: logging.info("Attached dummy camera.") # If camera has a lock Loading Loading @@ -173,8 +180,32 @@ class Microscope(object): return state @property def config(self): def apply_config(self, config: dict): """ 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 self.camera: # Update camera config params from StreamingCamera object Loading @@ -196,27 +227,12 @@ class Microscope(object): self.rc.update(backlash) return self.rc.config return self.rc.asdict() @property def config(self) -> dict: return self.read_config() @config.setter def config(self, config: dict) -> None: """ 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() No newline at end of file self.apply_config(config) Loading
openflexure_microscope/camera/pi.py +23 −37 Original line number Diff line number Diff line Loading @@ -25,13 +25,7 @@ from __future__ import division import io import time import datetime import sys import os import yaml import copy import numpy as np from PIL import Image import logging # Used for conversion only Loading @@ -44,9 +38,6 @@ import picamera.array # Type hinting from typing import Tuple # Threading import threading from .base import BaseCamera, CaptureObject # Richard's fix gain from .set_picamera_gain import set_analog_gain, set_digital_gain Loading @@ -70,9 +61,9 @@ CONFIG_KEYS = { }, } # Useful methods # TODO: Remove this def fractions_to_floats(value): """Deal with horrible, horrible PiCamera fractions""" result = value Loading @@ -85,15 +76,15 @@ def fractions_to_floats(value): result = float(value) return result # MAIN CLASS # MAIN CLASS class StreamingCamera(BaseCamera): """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, picamera_settings: dict=None): def __init__(self, config: dict = None): global CONFIG_KEYS # Run BaseCamera init Loading @@ -111,11 +102,14 @@ class StreamingCamera(BaseCamera): self.set_zoom(1.0) # Populate config and settings with all available keys self._config.update(CONFIG_KEYS) self.config.update(CONFIG_KEYS) # Load config dictionary if passed if config: self.config = config self.apply_config(config) # Create an empty stream self.stream = io.BytesIO() # Start streaming self.start_worker() Loading @@ -128,7 +122,7 @@ class StreamingCamera(BaseCamera): """Close the Raspberry Pi StreamingCamera.""" # Run BaseCamera close method BaseCamera.close(self) # Detatch Pi camera # Detach Pi camera if self.camera: self.camera.close() Loading @@ -146,8 +140,7 @@ class StreamingCamera(BaseCamera): """ return hasattr(self.camera, "lens_shading_table") @property def config(self): def read_config(self): """ Return config dictionary of the StreamingCamera. """ Loading @@ -156,7 +149,7 @@ class StreamingCamera(BaseCamera): } # PiCamera parameters (obtained directly from PiCamera object) for key, _ in self._config['picamera_settings'].items(): for key, _ in self.config['picamera_settings'].items(): try: value = getattr(self.camera, key) value = fractions_to_floats(value) Loading @@ -166,13 +159,12 @@ class StreamingCamera(BaseCamera): # StreamingCamera parameters (obtained from StreamingCamera _config) for key in CONFIG_KEYS: if (key != 'picamera_settings') and (key in self._config): conf_dict[key] = self._config[key] if (key != 'picamera_settings') and (key in self.config): conf_dict[key] = self.config[key] return conf_dict @config.setter def config(self, config: dict) -> None: def apply_config(self, config: dict) -> None: """ Write a config dictionary to the StreamingCamera config. Loading Loading @@ -202,7 +194,7 @@ class StreamingCamera(BaseCamera): # PiCamera parameters (applied directly to PiCamera object) if 'picamera_settings' in config: # If new settings are given 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])) # Handle special attributes: Loading @@ -217,7 +209,7 @@ class StreamingCamera(BaseCamera): for key, value in config.items(): # For each provided setting if key != 'picamera_settings': # We already handled this logging.debug("Setting parameter {}: {}".format(key, value)) self._config[key] = value self.config[key] = value # If stream was paused to update config, unpause if paused_stream: Loading @@ -230,7 +222,7 @@ class StreamingCamera(BaseCamera): 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: self.state['zoom_value'] = float(zoom_value) Loading Loading @@ -275,9 +267,8 @@ class StreamingCamera(BaseCamera): Args: 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. quality (int): Video recording quality. Returns: output_object (str/BytesIO): Target object. Loading Loading @@ -317,7 +308,7 @@ class StreamingCamera(BaseCamera): until the current recording has stopped.") return None def stop_recording(self) -> bool: def stop_recording(self): """Stop the last started video recording on splitter port 2.""" with self.lock: # Stop the camera video recording on port 2 Loading Loading @@ -364,7 +355,7 @@ class StreamingCamera(BaseCamera): 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']`. """ # If no stream object exists # If stream object was destroyed if not hasattr(self, 'stream'): self.stream = io.BytesIO() # Create a stream object Loading Loading @@ -404,6 +395,7 @@ class StreamingCamera(BaseCamera): use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. fmt (str): Format of the capture. resize ((int, int)): Resize the captured image. bayer (bool): Store raw bayer data in capture """ with self.lock: Loading @@ -416,7 +408,7 @@ class StreamingCamera(BaseCamera): 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: self.stop_stream_recording() Loading @@ -428,7 +420,7 @@ class StreamingCamera(BaseCamera): bayer=(not use_video_port) and bayer, 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: self.start_stream_recording() Loading @@ -436,7 +428,6 @@ class StreamingCamera(BaseCamera): def yuv( self, rgb=False, use_video_port: bool = True, resize: Tuple[int, int] = None) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Loading Loading @@ -473,10 +464,6 @@ class StreamingCamera(BaseCamera): if not use_video_port: self.start_stream_recording() if rgb: logging.debug("Converting to RGB") return output.rgb_array else: return output.array def array( Loading Loading @@ -537,7 +524,6 @@ class StreamingCamera(BaseCamera): # Update state logging.debug("STREAM ACTIVE") #self.state['stream_active'] = True # While the iterator is not closed try: Loading
openflexure_microscope/microscope.py +50 −34 Original line number Diff line number Diff line Loading @@ -25,22 +25,29 @@ class Microscope(object): Args: camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object microscope (: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. stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object config_path (str): Path to a microscoperc.yaml runtime-config file 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 self.rc = OpenflexureConfig(config_path=config_path, expand=True) # Attach initial hardware (may be NoneTypes) self.camera = None self.stage = None self.attach(camera, stage) # Create a 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 # Attach plugins Loading @@ -48,16 +55,15 @@ class Microscope(object): self.attach_plugins() # Set default parameters if not 'name' in self.rc.config: self.rc.config['name'] = uuid.uuid4().hex if 'name' not in self.rc.config: self.rc.update({'name': uuid.uuid4().hex}) if not 'fov' in self.rc.config: self.rc.config['fov'] = [0, 0] if 'fov' not in self.rc.config: self.rc.update({'fov': [0, 0]}) # 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 def __enter__(self): """Create microscope on context enter.""" return self Loading @@ -82,14 +88,15 @@ 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? stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object """ logging.debug("Attaching camera...") self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object if isinstance(self.camera, StreamingCamera): logging.info("Attached camera {}".format(camera)) logging.info("Updating camera settings") self.apply_config(self.camera.read_config()) elif not self.camera: logging.info("Attached dummy camera.") # If camera has a lock Loading Loading @@ -173,8 +180,32 @@ class Microscope(object): return state @property def config(self): def apply_config(self, config: dict): """ 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 self.camera: # Update camera config params from StreamingCamera object Loading @@ -196,27 +227,12 @@ class Microscope(object): self.rc.update(backlash) return self.rc.config return self.rc.asdict() @property def config(self) -> dict: return self.read_config() @config.setter def config(self, config: dict) -> None: """ 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() No newline at end of file self.apply_config(config)