Moved generic camera methods into BaseCamera class

This commit is contained in:
Joel Collins 2018-11-07 10:54:39 +00:00
parent 35c7d97db9
commit 7b23ca4b8a
4 changed files with 325 additions and 317 deletions

View file

@ -1,11 +1,8 @@
import time import time
import os
import io import io
import threading import threading
import copy
import datetime
import uuid
from PIL import Image from PIL import Image
import logging
try: try:
from greenlet import getcurrent as get_ident from greenlet import getcurrent as get_ident
@ -15,221 +12,22 @@ except ImportError:
except ImportError: except ImportError:
from _thread import get_ident from _thread import get_ident
# Slightly crappy debugger logging from .capture import StreamObject
DEBUG = True
def log(s): def last_entry(object_list: list):
if DEBUG: if object_list: # If any images have been captured
print('DEBUG: {}'.format(s)) return object_list[-1] # Return the latest captured image
else:
return None
class StreamObject(object): def entry_by_id(id: str, object_list: list):
def __init__(self, write_to_file: bool=None, filename: str=None, folder: str=None, fmt: str='file') -> None: found = None
# Store a nice ID for o in object_list:
self.id = uuid.uuid4().hex if o.id == id:
log("Created {}".format(self.id)) found = o
return found
# Create file name
iterator = 0
f_name = self.build_file_path(filename, folder, fmt)
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
self.file = f_name
# Byte stream properties
self.stream = io.BytesIO() # Byte stream that data will be written to
# Set default write target
self.write_to_file = write_to_file
if self.write_to_file is False:
log("Default target for {} set to 'stream'".format(self.id))
self.target = self.stream
else:
log("Default target for {} set to 'file'".format(self.id))
self.target = self.file
# Keep on disk after close by default
self.keep_on_disk = True
# Log if created by context manager
self.context_manager = False
# Object lock
self.locked = False # Is the StreamObject locked for writing? (Handled by StreamingCamera)
def __enter__(self):
log("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.
self.context_manager = True # Used in metadata
return self
def __exit__(self, *args):
log("Cleaning up {}".format(self.id))
self.close()
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.
Defaults to datestamp. Iterator adds a numeric increment to the file name.
"""
if filename:
file_name = "{}.{}".format(filename, fmt)
else:
file_name_base = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if iterator:
file_name_base = "{}_{}".format(file_name_base, iterator)
file_name = "{}.{}".format(file_name_base, fmt)
# Create folder and file
if folder:
if not os.path.exists(folder):
os.mkdir(folder)
file_path = os.path.join(folder, file_name)
else:
file_path = file_name
return file_path
def lock(self):
"""Set locked flag to True."""
self.locked = True
def unlock(self):
"""Set locked flag to False."""
self.locked = False
@property
def stream_exists(self, auto_rewind=True) -> bool:
"""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
self.stream.seek(0) # Rewind the data bytes for reading
return True
else:
self.stream.seek(0) # Rewind the data bytes for reading
return False
@property
def file_exists(self) -> bool:
"""Check if corresponding file exists"""
if os.path.isfile(self.file):
return True
else:
return False
@property
def metadata(self) -> dict:
d = {
'id': self.id,
'locked': self.locked,
'keep_on_disk': self.keep_on_disk,
'path': self.file,
'context_manager': self.context_manager,
}
# Get file path
if self.file_exists:
d['file'] = self.file
else:
d['file'] = None
# Check stream
if self.stream_exists:
d['stream'] = True
else:
d['stream'] = False
# Check if file was manually deleted
if self.keep_on_disk and not self.file_exists:
d['path'] = "{} (Deleted)".format(d['path'])
return d
@property
def data(self) -> io.BytesIO:
"""Return a byte string of the capture data."""
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
else: # If data stream is empty
if self.file_exists: # If data file exists
# TODO: Streamline this bit
log("No stream data. 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.seek(0) # Rewind loaded stream
data = io.BytesIO(d.getbuffer()) # Create a copy of the stream bytes
else:
data = None
return data # Read and return bytes data
@property
def binary(self) -> bytes:
"""Return a byte string of the capture data."""
return self.data.getvalue()
def load_file(self) -> bool:
"""Load data stored on disk to the in-memory stream."""
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.seek(0) # Rewind data bytes again
return True
else:
return False
def save_file(self) -> bool:
"""
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
log("Writing stream to file {}".format(self.file))
f.seek(0, 0) # Seek to the start of the file
f.write(self.binary) # Write data bytes to file
return True
else:
return False
def delete_stream(self):
"""Clears 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 os.path.isfile(self.file):
log("Deleting file {}".format(self.file))
os.remove(self.file)
return True
else:
return False
def shunt(self):
"""Demote the StreamObject from being stored in memory."""
if not self.file_exists: # If file doesn't already exist
self.save_file() # Save stream to disk, if it exists
self.delete_stream() # Delete the stream from memory
def close(self):
"""Both clear the stream, and delete any associated on-disk data"""
log("Closing {}".format(self.id))
self.delete_stream()
if not self.keep_on_disk:
self.delete_file()
class CameraEvent(object): class CameraEvent(object):
@ -282,10 +80,36 @@ class BaseCamera(object):
last_access = 0 # Time of last client access to the camera last_access = 0 # Time of last client access to the camera
event = CameraEvent() event = CameraEvent()
# CONTEXT MANAGER AND OBJECT CLEANUP
def __init__(self): def __init__(self):
"""Base implementation of StreamingCamera."""
self.state = {} # Create dict for capture state self.state = {} # Create dict for capture state
self.settings = {} # Create dict to store settings self.settings = {} # Create dict to store settings
# Capture data
self.images = []
self.videos = []
def __enter__(self):
"""Create camera on context enter."""
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Close camera stream on context exit."""
self.close()
def close(self):
"""Handles closing the StreamingCamera"""
# Close all StreamObjects
for capture_list in [self.images, self.videos]:
for stream_object in capture_list:
stream_object.close()
# Stop worker thread
self.stop_worker()
# START AND STOP WORKER THREAD
def start_worker(self, timeout: int=5) -> bool: def start_worker(self, timeout: int=5) -> bool:
"""Start the background camera thread if it isn't running yet.""" """Start the background camera thread if it isn't running yet."""
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
@ -299,7 +123,7 @@ class BaseCamera(object):
self.thread.start() self.thread.start()
# wait until frames are available # wait until frames are available
log("Waiting for frames") logging.info("Waiting for frames")
while self.get_frame() is None: while self.get_frame() is None:
if time.time() > timeout_time: if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for frames.") raise TimeoutError("Timeout waiting for frames.")
@ -319,6 +143,8 @@ class BaseCamera(object):
time.sleep(0) time.sleep(0)
return True return True
# HANDLE STREAM FRAMES
def get_frame(self): def get_frame(self):
"""Return the current camera frame.""" """Return the current camera frame."""
self.last_access = time.time() self.last_access = time.time()
@ -333,16 +159,45 @@ class BaseCamera(object):
"""Generator that returns frames from the camera.""" """Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.') raise RuntimeError('Must be implemented by subclasses.')
def close(self): # RETURNING CAPTURES
"""Handles closing the StreamingCamera"""
self.stop_worker()
def __enter__(self): @property
return self def image(self):
"""Return the latest captured image"""
return last_entry(self.images)
def __exit__(self, exc_type, exc_value, traceback): @property
"""Close camera stream on context exit.""" def video(self):
self.close() """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 entry_by_id(id, self.images)
def video_from_id(self, id):
"""Returns 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"""
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
target_list.append(stream_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)
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)
# WORKER THREAD
def _thread(self): def _thread(self):
"""Camera background thread.""" """Camera background thread."""
@ -359,8 +214,8 @@ class BaseCamera(object):
# if there hasn't been any clients asking for frames in # if there hasn't been any clients asking for frames in
# the last 10 seconds then stop the thread # the last 10 seconds then stop the thread
if time.time() - self.last_access > 20: if time.time() - self.last_access > 20:
self.frames_iterator.close() self.frames_iterator.close()
break break
try: try:
if self.stop is True: if self.stop is True:

View file

@ -0,0 +1,213 @@
import uuid
import io
import os
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:
# Store a nice ID
self.id = uuid.uuid4().hex
logging.info("Created {}".format(self.id))
# Create file name
iterator = 0
f_name = self.build_file_path(filename, folder, fmt)
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
self.file = f_name
# Byte stream properties
self.stream = io.BytesIO() # Byte stream that data will be written to
# 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))
self.target = self.stream
else:
logging.debug("Default target for {} set to 'file'".format(self.id))
self.target = self.file
# Keep on disk after close by default
self.keep_on_disk = True
# Log if created by context manager
self.context_manager = False
# Object lock
self.locked = False # Is the StreamObject locked for writing? (Handled by StreamingCamera)
def __enter__(self):
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.
self.context_manager = True # Used in metadata
return self
def __exit__(self, *args):
logging.info("Cleaning up {}".format(self.id))
self.close()
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.
Defaults to datestamp. Iterator adds a numeric increment to the file name.
"""
if filename:
file_name = "{}.{}".format(filename, fmt)
else:
file_name_base = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if iterator:
file_name_base = "{}_{}".format(file_name_base, iterator)
file_name = "{}.{}".format(file_name_base, fmt)
# Create folder and file
if folder:
if not os.path.exists(folder):
os.mkdir(folder)
file_path = os.path.join(folder, file_name)
else:
file_path = file_name
return file_path
def lock(self):
"""Set locked flag to True."""
self.locked = True
def unlock(self):
"""Set locked flag to False."""
self.locked = False
@property
def stream_exists(self, auto_rewind=True) -> bool:
"""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
self.stream.seek(0) # Rewind the data bytes for reading
return True
else:
self.stream.seek(0) # Rewind the data bytes for reading
return False
@property
def file_exists(self) -> bool:
"""Check if corresponding file exists"""
if os.path.isfile(self.file):
return True
else:
return False
@property
def metadata(self) -> dict:
d = {
'id': self.id,
'locked': self.locked,
'keep_on_disk': self.keep_on_disk,
'path': self.file,
'context_manager': self.context_manager,
}
# Get file path
if self.file_exists:
d['file'] = self.file
else:
d['file'] = None
# Check stream
if self.stream_exists:
d['stream'] = True
else:
d['stream'] = False
# Check if file was manually deleted
if self.keep_on_disk and not self.file_exists:
d['path'] = "{} (Deleted)".format(d['path'])
return d
@property
def data(self) -> io.BytesIO:
"""Return a byte string of the capture data."""
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
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))
with open(self.file, 'rb') as f:
d = io.BytesIO(f.read()) # Load bytes from file in disk
d.seek(0) # Rewind loaded stream
data = io.BytesIO(d.getbuffer()) # Create a copy of the stream bytes
else:
data = None
return data # Read and return bytes data
@property
def binary(self) -> bytes:
"""Return a byte string of the capture data."""
return self.data.getvalue()
def load_file(self) -> bool:
"""Load data stored on disk to the in-memory stream."""
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.seek(0) # Rewind data bytes again
return True
else:
return False
def save_file(self) -> bool:
"""
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))
f.seek(0, 0) # Seek to the start of the file
f.write(self.binary) # Write data bytes to file
return True
else:
return False
def delete_stream(self):
"""Clears 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 os.path.isfile(self.file):
logging.info("Deleting file {}".format(self.file))
os.remove(self.file)
return True
else:
return False
def shunt(self):
"""Demote the StreamObject from being stored in memory."""
if not self.file_exists: # If file doesn't already exist
self.save_file() # Save stream to disk, if it exists
self.delete_stream() # Delete the stream from memory
def close(self):
"""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:
self.delete_file()

View file

@ -32,6 +32,7 @@ import yaml
import copy import copy
import numpy as np import numpy as np
from PIL import Image from PIL import Image
import logging
# Pi camera # Pi camera
import picamera import picamera
@ -43,7 +44,7 @@ from typing import Tuple
# Threading # Threading
import threading import threading
from .base import BaseCamera, StreamObject, log from .base import BaseCamera, StreamObject
# 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
# Manage config # Manage config
@ -53,29 +54,12 @@ HERE = os.path.abspath(os.path.dirname(__file__))
DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml') DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml')
def last_entry(object_list: list):
if object_list: # If any images have been captured
return object_list[-1] # Return the latest captured image
else:
return None
def entry_by_id(id: str, object_list: list):
found = None
for o in object_list:
if o.id == id:
found = o
return found
class StreamingCamera(BaseCamera): class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera."""
def __init__(self): def __init__(self):
# Run BaseCamera init to start thread """Raspberry Pi camera implementation of StreamingCamera."""
# Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Capture data
self.images = []
self.videos = []
# Camera settings # Camera settings
self.settings.update({ self.settings.update({
'video_resolution': (832, 624), 'video_resolution': (832, 624),
@ -97,54 +81,8 @@ class StreamingCamera(BaseCamera):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
# HANDLE CONTEXT MANAGER AND FILE CLOSING
def close(self): # HANDLE SETTINGS
"""Close method called by BaseCamera __exit__"""
# Close all StreamObjects
for capture_list in [self.images, self.videos]:
for stream_object in capture_list:
stream_object.close()
self.stop_worker()
# RETURNING CAPTURES
@property
def image(self):
"""Return the latest captured image"""
return last_entry(self.images)
@property
def video(self):
"""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 entry_by_id(id, self.images)
def video_from_id(self, id):
"""Returns 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"""
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
target_list.append(stream_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)
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)
# HANDLING SETTINGS
# TODO: Handle exceptions # TODO: Handle exceptions
# TODO: Have this take a dictionary (not a config file) # TODO: Have this take a dictionary (not a config file)
@ -162,8 +100,7 @@ class StreamingCamera(BaseCamera):
else: else:
config = load_config(config_path) config = load_config(config_path)
log("Applying config:") logging.debug(config)
log(config)
# StreamingCamera settings # StreamingCamera settings
if 'video_resolution' in config: if 'video_resolution' in config:
@ -282,13 +219,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
log("Starting record at {}".format(self.settings['video_resolution'])) logging.info("Starting record at {}".format(self.settings['video_resolution']))
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
@ -303,9 +241,9 @@ class StreamingCamera(BaseCamera):
"""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
log("Stopping recording") logging.info("Stopping recording")
self.camera.stop_recording(splitter_port=2) self.camera.stop_recording(splitter_port=2)
log("Recording stopped") logging.info("Recording stopped")
# Update state dictionary # Update state dictionary
self.state['record_active'] = False self.state['record_active'] = False
@ -318,7 +256,7 @@ class StreamingCamera(BaseCamera):
resolution ((int, int)): Resolution to set the camera to, resolution ((int, int)): Resolution to set the camera to,
after stopping recording. after stopping recording.
""" """
log("Pausing stream") logging.debug("Pausing stream")
# If no resolution is specified, default to image_resolution # If no resolution is specified, default to image_resolution
if not resolution: if not resolution:
resolution = self.settings['image_resolution'] resolution = self.settings['image_resolution']
@ -337,7 +275,7 @@ class StreamingCamera(BaseCamera):
resolution ((int, int)): Resolution to set the camera to, resolution ((int, int)): Resolution to set the camera to,
before starting recording. before starting recording.
""" """
log("Unpausing stream") logging.debug("Unpausing stream")
if not resolution: if not resolution:
resolution = self.settings['video_resolution'] resolution = self.settings['video_resolution']
@ -389,7 +327,7 @@ class StreamingCamera(BaseCamera):
else: else:
target_obj = target target_obj = target
log("Capturing to {}".format(target)) logging.info("Capturing to {}".format(target))
if not use_video_port: if not use_video_port:
@ -441,9 +379,9 @@ class StreamingCamera(BaseCamera):
if not use_video_port: if not use_video_port:
self.pause_stream_for_capture(resolution=resolution) self.pause_stream_for_capture(resolution=resolution)
log("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:
log("Capturing to PiYUVArray from {}".format(self.camera)) logging.debug("Capturing to PiYUVArray from {}".format(self.camera))
self.camera.capture( self.camera.capture(
output, output,
@ -451,13 +389,13 @@ class StreamingCamera(BaseCamera):
format='yuv', format='yuv',
use_video_port=use_video_port) use_video_port=use_video_port)
log("Capturing complete") logging.debug("Capturing complete")
if not use_video_port: if not use_video_port:
self.resume_stream_for_capture() self.resume_stream_for_capture()
if rgb: if rgb:
log("Converting to RGB") logging.debug("Converting to RGB")
return output.rgb_array return output.rgb_array
else: else:
return output.array return output.array
@ -473,7 +411,6 @@ class StreamingCamera(BaseCamera):
""" """
# Run this initialisation method # Run this initialisation method
self.initialisation() self.initialisation()
self.stream_method = 'PiCamera'
with picamera.PiCamera() as self.camera: with picamera.PiCamera() as self.camera:
# Let camera warm up # Let camera warm up

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera, StreamObject, log from openflexure_microscope.camera.pi import StreamingCamera, StreamObject
import os import os
import io import io
import sys import sys
@ -12,6 +12,9 @@ from PIL import Image
import unittest import unittest
from pprint import pprint from pprint import pprint
import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
success_string = """ success_string = """
/O /O
| | | |