PEP-8-ified code a bit

This commit is contained in:
Joel Collins 2018-11-07 14:22:42 +00:00
parent 54f906f525
commit 5df9b53a9c
4 changed files with 122 additions and 72 deletions

View file

@ -16,6 +16,7 @@ from .capture import StreamObject
def last_entry(object_list: list): 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 if object_list: # If any images have been captured
return object_list[-1] # Return the latest captured image return object_list[-1] # Return the latest captured image
else: else:
@ -23,6 +24,7 @@ def last_entry(object_list: list):
def entry_by_id(id: str, object_list: list): def entry_by_id(id: str, object_list: list):
"""Return an object from a list, if <object>.id matches id argument."""
found = None found = None
for o in object_list: for o in object_list:
if o.id == id: if o.id == id:
@ -31,14 +33,17 @@ def entry_by_id(id: str, object_list: list):
class CameraEvent(object): class CameraEvent(object):
"""An Event-like class that signals all active clients when a new frame is
available.
"""
def __init__(self): 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 = {} self.events = {}
def wait(self, timeout: int=5): 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() ident = get_ident()
if ident not in self.events: if ident not in self.events:
# this is a new client # this is a new client
@ -48,7 +53,7 @@ class CameraEvent(object):
return self.events[ident][0].wait(timeout) return self.events[ident][0].wait(timeout)
def set(self): def set(self):
"""Invoked by the camera thread when a new frame is available.""" """Signal that a new frame is available."""
now = time.time() now = time.time()
remove = None remove = None
for ident, event in self.events.items(): for ident, event in self.events.items():
@ -68,7 +73,7 @@ class CameraEvent(object):
del self.events[remove] del self.events[remove]
def clear(self): 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() self.events[get_ident()][0].clear()
@ -98,7 +103,7 @@ class BaseCamera(object):
self.close() self.close()
def close(self): def close(self):
"""Close the BaseCamera and all attached StreamObjects""" """Close the BaseCamera and all attached StreamObjects."""
# Close all StreamObjects # Close all StreamObjects
for capture_list in [self.images, self.videos]: for capture_list in [self.images, self.videos]:
for stream_object in capture_list: for stream_object in capture_list:
@ -139,14 +144,14 @@ class BaseCamera(object):
return True return True
def stop_worker(self, timeout: int=5) -> bool: 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") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
self.stop = True self.stop = True
while self.thread: while self.thread:
if time.time() > timeout_time: if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for worker thread to close.") raise TimeoutError("Timeout waiting for worker thread close.")
else: else:
time.sleep(0) time.sleep(0)
return True return True
@ -164,33 +169,37 @@ class BaseCamera(object):
return self.frame return self.frame
def frames(self): 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.') raise RuntimeError('Must be implemented by subclasses.')
# RETURNING CAPTURES # RETURNING CAPTURES
@property @property
def image(self): def image(self):
"""Return the latest captured image""" """Return the latest captured image."""
return last_entry(self.images) return last_entry(self.images)
@property @property
def video(self): def video(self):
"""Return the latest recorded video""" """Return the latest recorded video."""
return last_entry(self.videos) return last_entry(self.videos)
def image_from_id(self, id): 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) return entry_by_id(id, self.images)
def video_from_id(self, id): 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) return entry_by_id(id, self.videos)
# CREATING NEW CAPTURES # CREATING NEW CAPTURES
def new_stream_object(self, stream_object, target_list: list, shunt_others: bool=True): def new_stream_object(
"""Add a new capture to a list of captures, and shunt all others""" 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 if shunt_others: # If shunting all older captures
for obj in target_list: # For each older capture for obj in target_list: # For each older capture
obj.shunt() # Shunt capture from memory to storage obj.shunt() # Shunt capture from memory to storage
@ -198,12 +207,18 @@ class BaseCamera(object):
return stream_object return stream_object
def new_image(self, stream_object, shunt_others=True): def new_image(self, stream_object, shunt_others=True):
"""Add a new capture to the image list, and shunt all 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) return self.new_stream_object(
stream_object,
self.images,
shunt_others=shunt_others)
def new_video(self, stream_object, shunt_others=True): def new_video(self, stream_object, shunt_others=True):
"""Add a new capture to the video list, and shunt all 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) return self.new_stream_object(
stream_object,
self.videos,
shunt_others=shunt_others)
# WORKER THREAD # WORKER THREAD

View file

@ -5,8 +5,15 @@ import datetime
import copy import copy
import logging import logging
class StreamObject(object): 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 # Store a nice ID
self.id = uuid.uuid4().hex self.id = uuid.uuid4().hex
logging.info("Created {}".format(self.id)) logging.info("Created {}".format(self.id))
@ -17,7 +24,11 @@ class StreamObject(object):
while os.path.isfile(f_name): # While file already exists while os.path.isfile(f_name): # While file already exists
iterator += 1 # Add a file name iterator 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 self.file = f_name
@ -27,10 +38,10 @@ class StreamObject(object):
# Set default write target # Set default write target
self.write_to_file = write_to_file self.write_to_file = write_to_file
if self.write_to_file is False: 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 self.target = self.stream
else: 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 self.target = self.file
# Keep on disk after close by default # Keep on disk after close by default
@ -40,9 +51,10 @@ class StreamObject(object):
self.context_manager = False self.context_manager = False
# Object lock # Object lock
self.locked = False # Is the StreamObject locked for writing? (Handled by StreamingCamera) self.locked = False
def __enter__(self): def __enter__(self):
"""Create StreamObject in context, to auto-clean disk data."""
logging.debug("Entering context for {}.\ logging.debug("Entering context for {}.\
Stored files will be cleaned up automatically.".format(self.id)) Stored files will be cleaned up automatically.".format(self.id))
self.keep_on_disk = False # Flag file to be removed on close. self.keep_on_disk = False # Flag file to be removed on close.
@ -50,14 +62,21 @@ class StreamObject(object):
return self return self
def __exit__(self, *args): def __exit__(self, *args):
"""Exit StreamObject, and auto-clean disk data."""
logging.info("Cleaning up {}".format(self.id)) logging.info("Cleaning up {}".format(self.id))
self.close() 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: if filename:
file_name = "{}.{}".format(filename, fmt) file_name = "{}.{}".format(filename, fmt)
@ -88,7 +107,7 @@ class StreamObject(object):
@property @property
def stream_exists(self, auto_rewind=True) -> bool: def stream_exists(self, auto_rewind=True) -> bool:
"""Check if BytesIO stream is empty""" """Check if BytesIO stream is empty."""
if auto_rewind: if auto_rewind:
self.stream.seek(0) # Rewind the data bytes for reading self.stream.seek(0) # Rewind the data bytes for reading
if self.stream.getvalue(): # If data stream contains data if self.stream.getvalue(): # If data stream contains data
@ -100,7 +119,7 @@ class StreamObject(object):
@property @property
def file_exists(self) -> bool: def file_exists(self) -> bool:
"""Check if corresponding file exists""" """Check if corresponding file exists."""
if os.path.isfile(self.file): if os.path.isfile(self.file):
return True return True
else: else:
@ -108,6 +127,7 @@ class StreamObject(object):
@property @property
def metadata(self) -> dict: def metadata(self) -> dict:
"""Return dictionary of StreamObject properties."""
d = { d = {
'id': self.id, 'id': self.id,
'locked': self.locked, 'locked': self.locked,
@ -140,16 +160,18 @@ class StreamObject(object):
self.stream.seek(0) # Rewind the data bytes for reading self.stream.seek(0) # Rewind the data bytes for reading
if self.stream_exists: # If data stream contains data 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 else: # If data stream is empty
if self.file_exists: # If data file exists if self.file_exists: # If data file exists
# TODO: Streamline this bit # 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: 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 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: else:
data = None data = None
@ -165,16 +187,14 @@ class StreamObject(object):
if self.file_exists: # If data file exists if self.file_exists: # If data file exists
# TODO: Streamline this bit # TODO: Streamline this bit
with open(self.file, 'rb') as f: 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 self.stream.seek(0) # Rewind data bytes again
return True return True
else: else:
return False return False
def save_file(self) -> bool: 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 if self.stream_exists: # If there's a stream to save
with open(self.file, 'ab') as f: # Load file as bytes with open(self.file, 'ab') as f: # Load file as bytes
logging.debug("Writing stream to file {}".format(self.file)) logging.debug("Writing stream to file {}".format(self.file))
@ -185,13 +205,11 @@ class StreamObject(object):
return False return False
def delete_stream(self): def delete_stream(self):
"""Clears the BytesIO stream of the StreamObject.""" """Clear the BytesIO stream of the StreamObject."""
self.stream = io.BytesIO() self.stream = io.BytesIO()
def delete_file(self) -> bool: def delete_file(self) -> bool:
""" """If the StreamObject has been saved, delete the file."""
If the StreamObject has been saved to disk, deletes the file and returns True.
"""
if os.path.isfile(self.file): if os.path.isfile(self.file):
logging.info("Deleting file {}".format(self.file)) logging.info("Deleting file {}".format(self.file))
os.remove(self.file) os.remove(self.file)
@ -206,7 +224,7 @@ class StreamObject(object):
self.delete_stream() # Delete the stream from memory self.delete_stream() # Delete the stream from memory
def close(self): 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)) logging.info("Closing {}".format(self.id))
self.delete_stream() self.delete_stream()
if not self.keep_on_disk: if not self.keep_on_disk:

View file

@ -84,7 +84,7 @@ class StreamingCamera(BaseCamera):
pass pass
def close(self): def close(self):
"""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 # Detatch Pi camera
@ -135,7 +135,9 @@ class StreamingCamera(BaseCamera):
if 'awb_mode' in config: if 'awb_mode' in config:
self.camera.awb_mode = config['awb_mode'] self.camera.awb_mode = config['awb_mode']
if 'red_gain' in config and 'blue_gain' in config: 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 # Camera framerate
if 'framerate' in config: if 'framerate' in config:
@ -158,7 +160,8 @@ class StreamingCamera(BaseCamera):
self.resume_stream_for_capture() self.resume_stream_for_capture()
else: 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: def change_zoom(self, zoom_value: int=1) -> None:
"""Change the camera zoom, handling recentering and scaling.""" """Change the camera zoom, handling recentering and scaling."""
@ -213,7 +216,6 @@ class StreamingCamera(BaseCamera):
fmt (str): Format of the capture. fmt (str): Format of the capture.
(default 'h264') (default 'h264')
""" """
# Start recording method only if a current recording is not running # Start recording method only if a current recording is not running
if not self.state['record_active']: if not self.state['record_active']:
@ -235,14 +237,14 @@ class StreamingCamera(BaseCamera):
target_obj = target target_obj = target
# Start the camera video recording on port 2 # 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( self.camera.start_recording(
target, target,
format=fmt, format=fmt,
splitter_port=2, splitter_port=2,
resize=self.settings['video_resolution'], resize=self.settings['video_resolution'],
quality=quality) quality=quality)
logging.debug("Recording started successfully.")
# Update state dictionary # Update state dictionary
self.state['record_active'] = True self.state['record_active'] = True
@ -250,12 +252,13 @@ class StreamingCamera(BaseCamera):
return target_obj return target_obj
else: 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 return None
def stop_recording(self) -> bool: def stop_recording(self) -> bool:
"""Stop the last started video recording on splitter port 2.""" """Stop the last started video recording on splitter port 2."""
# Stop the camera video recording on port 2 # Stop the camera video recording on port 2
logging.info("Stopping recording") logging.info("Stopping recording")
self.camera.stop_recording(splitter_port=2) self.camera.stop_recording(splitter_port=2)
@ -264,7 +267,10 @@ class StreamingCamera(BaseCamera):
# Update state dictionary # Update state dictionary
self.state['record_active'] = False 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. Pause capture on a splitter port.
@ -283,7 +289,10 @@ class StreamingCamera(BaseCamera):
# Increase the resolution for taking an image # Increase the resolution for taking an image
self.camera.resolution = resolution 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. Resume capture on a splitter port.
@ -332,11 +341,14 @@ class StreamingCamera(BaseCamera):
(default 'h264') (default 'h264')
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
# If no target is specified, store to StreamingCamera # If no target is specified, store to StreamingCamera
if not target: if not target:
# Create a new image and add to the image list # 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 target = target_obj.target # Store to the StreamObject BytesIO
else: else:
@ -373,13 +385,17 @@ class StreamingCamera(BaseCamera):
return target_obj 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. """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. resize ((int, int)): Resize the captured image.
""" """
if use_video_port: if use_video_port:
resolution = self.settings['video_resolution'] resolution = self.settings['video_resolution']
else: else:
@ -395,7 +411,8 @@ class StreamingCamera(BaseCamera):
logging.debug("Creating PiYUVArray") logging.debug("Creating PiYUVArray")
with picamera.array.PiYUVArray(self.camera, size=size) as output: 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( self.camera.capture(
output, output,
@ -403,8 +420,6 @@ class StreamingCamera(BaseCamera):
format='yuv', format='yuv',
use_video_port=use_video_port) use_video_port=use_video_port)
logging.debug("Capturing complete")
if not use_video_port: if not use_video_port:
self.resume_stream_for_capture() self.resume_stream_for_capture()
@ -418,10 +433,10 @@ class StreamingCamera(BaseCamera):
def frames(self): 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 Records video from port 1 to a byte stream,
byte stream, and iterates sequential frames. and iterates sequential frames.
""" """
# Run this initialisation method # Run this initialisation method
self.initialisation() self.initialisation()
@ -447,7 +462,8 @@ class StreamingCamera(BaseCamera):
logging.debug("STREAM ACTIVE") logging.debug("STREAM ACTIVE")
self.state['stream_active'] = True self.state['stream_active'] = True
try: # While the iterator is not closed # While the iterator is not closed
try:
while True: while True:
# reset stream for next frame # reset stream for next frame
self.stream.seek(0) self.stream.seek(0)
@ -462,7 +478,8 @@ class StreamingCamera(BaseCamera):
pass pass
else: else:
yield frame 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") logging.debug("Stopping stream recording on port 1")
self.camera.stop_recording(splitter_port=1) self.camera.stop_recording(splitter_port=1)
self.state['stream_active'] = False self.state['stream_active'] = False