diff --git a/openflexure_microscope/camera/__init__.py b/openflexure_microscope/camera/__init__.py index adfe60a7..9c7de675 100644 --- a/openflexure_microscope/camera/__init__.py +++ b/openflexure_microscope/camera/__init__.py @@ -1 +1 @@ -from . import pi, base \ No newline at end of file +from . import pi, base diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 93158ea7..2f02ee85 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -16,6 +16,7 @@ from .capture import StreamObject def last_entry(object_list: list): + """Return the last entry of a list, if the list contains items.""" if object_list: # If any images have been captured return object_list[-1] # Return the latest captured image else: @@ -23,6 +24,7 @@ def last_entry(object_list: list): def entry_by_id(id: str, object_list: list): + """Return an object from a list, if .id matches id argument.""" found = None for o in object_list: if o.id == id: @@ -31,14 +33,17 @@ def entry_by_id(id: str, object_list: list): class CameraEvent(object): - """An Event-like class that signals all active clients when a new frame is - available. - """ def __init__(self): + """ + Create a frame-signaller object for StreamingCamera. + + An event-like class that signals all active clients + when a new frame is available. + """ self.events = {} def wait(self, timeout: int=5): - """Invoked from each client's thread to wait for the next frame.""" + """Wait for the next frame (invoked from each client's thread).""" ident = get_ident() if ident not in self.events: # this is a new client @@ -48,7 +53,7 @@ class CameraEvent(object): return self.events[ident][0].wait(timeout) def set(self): - """Invoked by the camera thread when a new frame is available.""" + """Signal that a new frame is available.""" now = time.time() remove = None for ident, event in self.events.items(): @@ -68,7 +73,7 @@ class CameraEvent(object): del self.events[remove] def clear(self): - """Invoked from each client's thread after a frame was processed.""" + """Clear frame event, once processed.""" self.events[get_ident()][0].clear() @@ -98,7 +103,7 @@ class BaseCamera(object): self.close() def close(self): - """Close the BaseCamera and all attached StreamObjects""" + """Close the BaseCamera and all attached StreamObjects.""" # Close all StreamObjects for capture_list in [self.images, self.videos]: for stream_object in capture_list: @@ -139,14 +144,14 @@ class BaseCamera(object): return True def stop_worker(self, timeout: int=5) -> bool: - """Flags worker thread for stop. Waits for thread to close, or times out.""" + """Flag worker thread for stop. Waits for thread close or timeout.""" logging.debug("Stopping worker thread") timeout_time = time.time() + timeout self.stop = True while self.thread: if time.time() > timeout_time: - raise TimeoutError("Timeout waiting for worker thread to close.") + raise TimeoutError("Timeout waiting for worker thread close.") else: time.sleep(0) return True @@ -164,33 +169,37 @@ class BaseCamera(object): return self.frame def frames(self): - """Generator that returns frames from the camera.""" + """Create generator that returns frames from the camera.""" raise RuntimeError('Must be implemented by subclasses.') # RETURNING CAPTURES @property def image(self): - """Return the latest captured image""" + """Return the latest captured image.""" return last_entry(self.images) @property def video(self): - """Return the latest recorded video""" + """Return the latest recorded video.""" return last_entry(self.videos) def image_from_id(self, id): - """Returns an image StreamObject with a matching ID""" + """Return an image StreamObject with a matching ID.""" return entry_by_id(id, self.images) def video_from_id(self, id): - """Returns a video StreamObject with a matching ID""" + """Return a video StreamObject with a matching ID.""" return entry_by_id(id, self.videos) # CREATING NEW CAPTURES - def new_stream_object(self, stream_object, target_list: list, shunt_others: bool=True): - """Add a new capture to a list of captures, and shunt all others""" + def new_stream_object( + self, + stream_object, + target_list: list, + shunt_others: bool=True): + """Add a new capture to a list of captures, and shunt all others.""" if shunt_others: # If shunting all older captures for obj in target_list: # For each older capture obj.shunt() # Shunt capture from memory to storage @@ -198,12 +207,18 @@ class BaseCamera(object): return stream_object def new_image(self, stream_object, shunt_others=True): - """Add a new capture to the image list, and shunt all others""" - return self.new_stream_object(stream_object, self.images, shunt_others=shunt_others) + """Add a new capture to the image list, and shunt all others.""" + return self.new_stream_object( + stream_object, + self.images, + shunt_others=shunt_others) def new_video(self, stream_object, shunt_others=True): - """Add a new capture to the video list, and shunt all others""" - return self.new_stream_object(stream_object, self.videos, shunt_others=shunt_others) + """Add a new capture to the video list, and shunt all others.""" + return self.new_stream_object( + stream_object, + self.videos, + shunt_others=shunt_others) # WORKER THREAD diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 501dfbdb..d2d0d698 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -5,8 +5,15 @@ import datetime import copy import logging + class StreamObject(object): - def __init__(self, write_to_file: bool=None, filename: str=None, folder: str=None, fmt: str='file') -> None: + def __init__( + self, + write_to_file: bool=None, + filename: str=None, + folder: str=None, + fmt: str='file') -> None: + """Create a new StreamObject, to manage capture data.""" # Store a nice ID self.id = uuid.uuid4().hex logging.info("Created {}".format(self.id)) @@ -17,7 +24,11 @@ class StreamObject(object): while os.path.isfile(f_name): # While file already exists iterator += 1 # Add a file name iterator - f_name = self.build_file_path(filename, folder, fmt, iterator=iterator) # Rebuild file name + f_name = self.build_file_path( + filename, + folder, + fmt, + iterator=iterator) # Rebuild file name self.file = f_name @@ -27,10 +38,10 @@ class StreamObject(object): # Set default write target self.write_to_file = write_to_file if self.write_to_file is False: - logging.debug("Default target for {} set to 'stream'".format(self.id)) + logging.debug("Target for {} set to 'stream'".format(self.id)) self.target = self.stream else: - logging.debug("Default target for {} set to 'file'".format(self.id)) + logging.debug("Target for {} set to 'file'".format(self.id)) self.target = self.file # Keep on disk after close by default @@ -40,9 +51,10 @@ class StreamObject(object): self.context_manager = False # Object lock - self.locked = False # Is the StreamObject locked for writing? (Handled by StreamingCamera) + self.locked = False def __enter__(self): + """Create StreamObject in context, to auto-clean disk data.""" logging.debug("Entering context for {}.\ Stored files will be cleaned up automatically.".format(self.id)) self.keep_on_disk = False # Flag file to be removed on close. @@ -50,14 +62,21 @@ class StreamObject(object): return self def __exit__(self, *args): + """Exit StreamObject, and auto-clean disk data.""" logging.info("Cleaning up {}".format(self.id)) self.close() - def build_file_path(self, filename: str, folder: str, fmt: str, iterator: int=0) -> str: + def build_file_path( + self, + filename: str, + folder: str, + fmt: str, + iterator: int=0) -> str: """ - Construct a full file path, based on filename, folder, and file format. + Construct a full file path, based on filename, folder, and file format. - Defaults to datestamp. Iterator adds a numeric increment to the file name. + Defaults to datestamp. + Iterator adds a numeric increment to the file name. """ if filename: file_name = "{}.{}".format(filename, fmt) @@ -67,7 +86,7 @@ class StreamObject(object): file_name_base = "{}_{}".format(file_name_base, iterator) file_name = "{}.{}".format(file_name_base, fmt) - # Create folder and file + # Create folder and file if folder: if not os.path.exists(folder): os.mkdir(folder) @@ -88,7 +107,7 @@ class StreamObject(object): @property def stream_exists(self, auto_rewind=True) -> bool: - """Check if BytesIO stream is empty""" + """Check if BytesIO stream is empty.""" if auto_rewind: self.stream.seek(0) # Rewind the data bytes for reading if self.stream.getvalue(): # If data stream contains data @@ -100,7 +119,7 @@ class StreamObject(object): @property def file_exists(self) -> bool: - """Check if corresponding file exists""" + """Check if corresponding file exists.""" if os.path.isfile(self.file): return True else: @@ -108,6 +127,7 @@ class StreamObject(object): @property def metadata(self) -> dict: + """Return dictionary of StreamObject properties.""" d = { 'id': self.id, 'locked': self.locked, @@ -140,16 +160,18 @@ class StreamObject(object): self.stream.seek(0) # Rewind the data bytes for reading if self.stream_exists: # If data stream contains data - data = io.BytesIO(self.stream.getbuffer()) # Create a copy of the stream bytes + # Create a copy of the stream bytes + data = io.BytesIO(self.stream.getbuffer()) else: # If data stream is empty if self.file_exists: # If data file exists # TODO: Streamline this bit - logging.info("No stream data. Opening from file {}".format(self.file)) + logging.info("Opening from file {}".format(self.file)) with open(self.file, 'rb') as f: - d = io.BytesIO(f.read()) # Load bytes from file in disk + d = io.BytesIO(f.read()) # Load bytes from file d.seek(0) # Rewind loaded stream - data = io.BytesIO(d.getbuffer()) # Create a copy of the stream bytes + # Create a copy of the stream bytes + data = io.BytesIO(d.getbuffer()) else: data = None @@ -165,16 +187,14 @@ class StreamObject(object): if self.file_exists: # If data file exists # TODO: Streamline this bit with open(self.file, 'rb') as f: - self.stream = io.BytesIO(f.read()) # Load bytes from file in disk + self.stream = io.BytesIO(f.read()) # Load bytes from file self.stream.seek(0) # Rewind data bytes again return True else: return False def save_file(self) -> bool: - """ - Write the StreamObjects stream to a file. - """ + """Write the StreamObjects stream to a file.""" if self.stream_exists: # If there's a stream to save with open(self.file, 'ab') as f: # Load file as bytes logging.debug("Writing stream to file {}".format(self.file)) @@ -185,13 +205,11 @@ class StreamObject(object): return False def delete_stream(self): - """Clears the BytesIO stream of the StreamObject.""" + """Clear the BytesIO stream of the StreamObject.""" self.stream = io.BytesIO() def delete_file(self) -> bool: - """ - If the StreamObject has been saved to disk, deletes the file and returns True. - """ + """If the StreamObject has been saved, delete the file.""" if os.path.isfile(self.file): logging.info("Deleting file {}".format(self.file)) os.remove(self.file) @@ -206,7 +224,7 @@ class StreamObject(object): self.delete_stream() # Delete the stream from memory def close(self): - """Both clear the stream, and delete any associated on-disk data""" + """Both clear the stream, and delete any associated on-disk data.""" logging.info("Closing {}".format(self.id)) self.delete_stream() if not self.keep_on_disk: diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 5c21d909..b9b3279f 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -84,7 +84,7 @@ class StreamingCamera(BaseCamera): pass def close(self): - """Close the Raspberry Pi StreamingCamera""" + """Close the Raspberry Pi StreamingCamera.""" # Run BaseCamera close method BaseCamera.close(self) # Detatch Pi camera @@ -135,7 +135,9 @@ class StreamingCamera(BaseCamera): 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']) + self.camera.awb_gains = ( + config['red_gain'], + config['blue_gain']) # Camera framerate if 'framerate' in config: @@ -152,13 +154,14 @@ class StreamingCamera(BaseCamera): set_analog_gain(self.camera, config['analog_gain']) if 'digital_gain' in config: set_digital_gain(self.camera, config['digital_gain']) - + if paused_stream: # If stream was paused to update settings logging.info("Resuming stream.") self.resume_stream_for_capture() else: - raise Exception("Cannot update camera settings while recording is active.") + raise Exception( + "Cannot update camera settings while recording is active.") def change_zoom(self, zoom_value: int=1) -> None: """Change the camera zoom, handling recentering and scaling.""" @@ -213,7 +216,6 @@ class StreamingCamera(BaseCamera): fmt (str): Format of the capture. (default 'h264') """ - # Start recording method only if a current recording is not running if not self.state['record_active']: @@ -227,7 +229,7 @@ class StreamingCamera(BaseCamera): fmt=fmt)) # Lock the StreamObject while recording - target_obj.lock() + target_obj.lock() # Store to the StreamObject target target = target_obj.target @@ -235,14 +237,14 @@ class StreamingCamera(BaseCamera): target_obj = target # Start the camera video recording on port 2 - logging.info("Starting record at {}".format(self.settings['video_resolution'])) + logging.info("Recording to {}".format(target)) + self.camera.start_recording( target, format=fmt, splitter_port=2, resize=self.settings['video_resolution'], quality=quality) - logging.debug("Recording started successfully.") # Update state dictionary self.state['record_active'] = True @@ -250,12 +252,13 @@ class StreamingCamera(BaseCamera): return target_obj else: - print("Cannot start a new recording until the current recording has stopped.") + print( + "Cannot start a new recording\ + until the current recording has stopped.") return None def stop_recording(self) -> bool: """Stop the last started video recording on splitter port 2.""" - # Stop the camera video recording on port 2 logging.info("Stopping recording") self.camera.stop_recording(splitter_port=2) @@ -264,12 +267,15 @@ class StreamingCamera(BaseCamera): # Update state dictionary self.state['record_active'] = False - def pause_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None: + def pause_stream_for_capture( + self, + splitter_port: int=1, + resolution: Tuple[int, int]=None) -> None: """ Pause capture on a splitter port. splitter_port (int): Splitter port to stop recording on - resolution ((int, int)): Resolution to set the camera to, + resolution ((int, int)): Resolution to set the camera to, after stopping recording. """ logging.debug("Pausing stream") @@ -283,12 +289,15 @@ class StreamingCamera(BaseCamera): # Increase the resolution for taking an image self.camera.resolution = resolution - def resume_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None: + def resume_stream_for_capture( + self, + splitter_port: int=1, + resolution: Tuple[int, int]=None) -> None: """ Resume capture on a splitter port. splitter_port (int): Splitter port to start recording on - resolution ((int, int)): Resolution to set the camera to, + resolution ((int, int)): Resolution to set the camera to, before starting recording. """ logging.debug("Unpausing stream") @@ -332,11 +341,14 @@ class StreamingCamera(BaseCamera): (default 'h264') resize ((int, int)): Resize the captured image. """ - # If no target is specified, store to StreamingCamera if not target: # Create a new image and add to the image list - target_obj = self.new_image(StreamObject(write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt)) + target_obj = self.new_image(StreamObject( + write_to_file=write_to_file, + filename=filename, + folder=folder, + fmt=fmt)) target = target_obj.target # Store to the StreamObject BytesIO else: @@ -373,13 +385,17 @@ class StreamingCamera(BaseCamera): return target_obj - def array(self, rgb=False, use_video_port: bool=True, resize: Tuple[int, int]=None) -> np.ndarray: + def array( + 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. - 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) resize ((int, int)): Resize the captured image. """ - if use_video_port: resolution = self.settings['video_resolution'] else: @@ -395,7 +411,8 @@ class StreamingCamera(BaseCamera): logging.debug("Creating PiYUVArray") with picamera.array.PiYUVArray(self.camera, size=size) as output: - logging.debug("Capturing to PiYUVArray from {}".format(self.camera)) + + logging.info("Capturing to {}".format(output)) self.camera.capture( output, @@ -403,8 +420,6 @@ class StreamingCamera(BaseCamera): format='yuv', use_video_port=use_video_port) - logging.debug("Capturing complete") - if not use_video_port: self.resume_stream_for_capture() @@ -418,10 +433,10 @@ class StreamingCamera(BaseCamera): def frames(self): """ - Create iterator used by worker thread to generate stream frames. + Create generator that returns frames from the camera. - Holds a Pi camera in context, records video from port 1 to a - byte stream, and iterates sequential frames. + Records video from port 1 to a byte stream, + and iterates sequential frames. """ # Run this initialisation method self.initialisation() @@ -447,7 +462,8 @@ class StreamingCamera(BaseCamera): logging.debug("STREAM ACTIVE") self.state['stream_active'] = True - try: # While the iterator is not closed + # While the iterator is not closed + try: while True: # reset stream for next frame self.stream.seek(0) @@ -462,7 +478,8 @@ class StreamingCamera(BaseCamera): pass else: yield frame - finally: # When GeneratorExit or StopIteration raised, run cleanup code + # When GeneratorExit or StopIteration raised, run cleanup code + finally: logging.debug("Stopping stream recording on port 1") self.camera.stop_recording(splitter_port=1) self.state['stream_active'] = False