diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 9d7a491a..8ab99e8a 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -41,34 +41,29 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain # Handle config and picamera settings CONFIG_KEYS = { - 'stream_resolution': (832, 624), - 'image_resolution': (2592, 1944), # Default for PiCamera v1. Overridden in __init__ - 'numpy_resolution': (1312, 976), - 'jpeg_quality': 75, + 'stream_resolution': tuple, + 'image_resolution': tuple, + 'numpy_resolution': tuple, + 'analog_gain': float, + 'digital_gain': float, + 'jpeg_quality': int, 'picamera_settings': { - 'exposure_mode': None, - 'awb_mode': None, - 'awb_gains': None, - 'framerate': None, - 'shutter_speed': None, - 'saturation': None, - 'analog_gain': None, - 'digital_gain': None, - 'lens_shading_table': None, + 'exposure_mode': str, + 'awb_mode': str, + 'awb_gains': tuple, + 'framerate': int, + 'shutter_speed': float, + 'saturation': float, + 'lens_shading_table': np.ndarray, }, } # 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): - global CONFIG_KEYS + """Raspberry Pi camera implementation of StreamingCamera.""" + def __init__(self): # Run BaseCamera init BaseCamera.__init__(self) # Attach to Pi camera @@ -83,17 +78,11 @@ class StreamingCamera(BaseCamera): # Reset variable states self.set_zoom(1.0) - # Populate config and settings with all available keys - self.config.update(CONFIG_KEYS) - - # Update config based on PiCamera parameters - self.config.update({ - 'image_resolution': tuple(self.camera.MAX_RESOLUTION) - }) - - # Load config dictionary if passed - if config: - self.apply_config(config) + # Update config properties + self.image_resolution = tuple(self.camera.MAX_RESOLUTION) + self.stream_resolution = (832, 624) + self.numpy_resolution = (1312, 976) + self.jpeg_quality = 75 # Create an empty stream self.stream = io.BytesIO() @@ -127,26 +116,47 @@ class StreamingCamera(BaseCamera): """ return hasattr(self.camera, "lens_shading_table") + @property + def digital_gain(self): + # TODO: Handle missing picamera? + return self.camera.digital_gain + + @digital_gain.setter + def digital_gain(self, value): + set_digital_gain(self.camera, value) + + @property + def analog_gain(self): + # TODO: Handle missing picamera? + return self.camera.analog_gain + + @analog_gain.setter + def analog_gain(self, value): + set_analog_gain(self.camera, value) + def read_config(self): """ Return config dictionary of the StreamingCamera. """ + global CONFIG_KEYS + conf_dict = { 'picamera_settings': {}, } - # PiCamera parameters (obtained directly from PiCamera object) - for key, _ in CONFIG_KEYS['picamera_settings'].items(): + # PiCamera parameters + for key in CONFIG_KEYS['picamera_settings']: try: value = getattr(self.camera, key) + logging.debug("{}: {}".format(key, value)) conf_dict['picamera_settings'][key] = value except AttributeError: logging.debug("Unable to read PiCamera attribute {}".format(key)) - # StreamingCamera parameters (obtained from StreamingCamera _config) + # StreamingCamera parameters 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 hasattr(self, key): + conf_dict[key] = getattr(self, key) return conf_dict @@ -160,8 +170,12 @@ class StreamingCamera(BaseCamera): Args: config (dict): Dictionary of config parameters. """ - + global CONFIG_KEYS + # TODO: Include timing and batching logic when applying PiCamera settings + paused_stream = False + logging.debug("Applying config:") + logging.debug(config) with self.lock: @@ -177,32 +191,22 @@ class StreamingCamera(BaseCamera): self.stop_stream_recording() # Pause stream paused_stream = True # Remember to unpause stream when done - # PiCamera parameters (applied directly to PiCamera object) + # PiCamera parameters if 'picamera_settings' in config: # If new settings are given for key, value in config['picamera_settings'].items(): # For each given setting if hasattr(self.camera, key): - self.config['picamera_settings'][key] = None # Add the key to the list of returned settings + if key not in CONFIG_KEYS['picamera_settings']: + CONFIG_KEYS['picamera_settings'][key] = type(value) logging.debug("Setting parameter {}: {}".format(key, config['picamera_settings'][key])) + setattr(self.camera, key, value) # Write setting to camera - # Handle special attributes: - if key == 'digital_gain': - set_digital_gain(self.camera, value) - elif key == 'analog_gain': - set_analog_gain(self.camera, value) - elif key == "shutter_speed": - # TODO: use types from CONFIG_KEYS? @jtc42 - self.camera.shutter_speed = int(value) - else: - setattr(self.camera, key, value) # Write setting to camera - - # StreamingCamera parameters (applied via StreamingCamera config) + # StreamingCamera parameters for key, value in config.items(): # For each provided setting - if key != 'picamera_settings': # We already handled this - if key not in CONFIG_KEYS.keys(): - logging.warn("{} is not in the streaming camera settings dictionary - adding it.") - #continue #TODO: filter settings somehow? + if (key != 'picamera_settings') and hasattr(self, key): + if key not in CONFIG_KEYS: + CONFIG_KEYS[key] = type(value) logging.debug("Setting parameter {}: {}".format(key, value)) - self.config[key] = value + setattr(self, key, value) # If stream was paused to update config, unpause if paused_stream: @@ -223,21 +227,21 @@ class StreamingCamera(BaseCamera): self.state['zoom_value'] = 1 # Richard's code for zooming ! fov = self.camera.zoom - centre = np.array([fov[0] + fov[2]/2.0, fov[1] + fov[3]/2.0]) - size = 1.0/self.state['zoom_value'] + centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) + size = 1.0 / self.state['zoom_value'] # If the new zoom value would be invalid, move the centre to # keep it within the camera's sensor (this is only relevant # when zooming out, if the FoV is not centred on (0.5, 0.5) for i in range(2): - if np.abs(centre[i] - 0.5) + size/2 > 0.5: - centre[i] = 0.5 + (1.0 - size)/2 * np.sign(centre[i]-0.5) + if np.abs(centre[i] - 0.5) + size / 2 > 0.5: + centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5) logging.info("setting zoom, centre {}, size {}".format(centre, size)) - new_fov = (centre[0] - size/2, centre[1] - size/2, size, size) + new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size) self.camera.zoom = new_fov # LAUNCH ACTIONS - def start_preview(self, fullscreen=True, window=None) -> bool: + def start_preview(self, fullscreen=True, window=None): """Start the on board GPU camera preview.""" logging.info("Starting the GPU preview") @@ -260,7 +264,7 @@ class StreamingCamera(BaseCamera): except picamera.exc.PiCameraValueError as e: logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e)) - def stop_preview(self) -> bool: + def stop_preview(self): """Stop the on board GPU camera preview.""" self.camera.stop_preview() self.state['preview_active'] = False @@ -289,8 +293,6 @@ class StreamingCamera(BaseCamera): # If output is a StreamObject if isinstance(output, CaptureObject): - # Lock the StreamObject while recording - output.lock() # Set target to capture stream output_stream = output.stream else: @@ -303,7 +305,7 @@ class StreamingCamera(BaseCamera): output_stream, format=fmt, splitter_port=2, - resize=self.config['stream_resolution'], + resize=self.stream_resolution, quality=quality) # Update state dictionary @@ -342,7 +344,7 @@ class StreamingCamera(BaseCamera): with self.lock: # If no resolution is specified, default to image_resolution if not resolution: - resolution = self.config['image_resolution'] + resolution = self.image_resolution # Stop the camera video recording on port 1 try: @@ -366,7 +368,7 @@ class StreamingCamera(BaseCamera): Args: 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['stream_resolution']`. + Defaults to `self.stream_resolution`. """ with self.lock: # If stream object was destroyed @@ -375,7 +377,7 @@ class StreamingCamera(BaseCamera): # If no explicit resolution is passed if not resolution: - resolution = self.config['stream_resolution'] # Default to video recording resolution + resolution = self.stream_resolution # Default to video recording resolution # Reduce the resolution for video streaming try: @@ -392,7 +394,7 @@ class StreamingCamera(BaseCamera): self.camera.start_recording( self.stream, format='mjpeg', - quality=self.config['jpeg_quality'], + quality=self.jpeg_quality, bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) @@ -463,9 +465,9 @@ class StreamingCamera(BaseCamera): """ with self.lock: if use_video_port: - resolution = self.config['stream_resolution'] + resolution = self.stream_resolution else: - resolution = self.config['numpy_resolution'] + resolution = self.numpy_resolution if resize: size = resize @@ -503,9 +505,9 @@ class StreamingCamera(BaseCamera): """ with self.lock: if use_video_port: - resolution = self.config['stream_resolution'] + resolution = self.stream_resolution else: - resolution = self.config['numpy_resolution'] + resolution = self.numpy_resolution if resize: size = resize @@ -557,7 +559,7 @@ class StreamingCamera(BaseCamera): self.stream.seek(0) self.stream.truncate() # to stream, read the new frame - time.sleep(1/self.camera.framerate*0.1) + time.sleep(1 / self.camera.framerate * 0.1) # yield the result to be read frame = self.stream.getvalue()