Improed CaptureObject documentation

This commit is contained in:
Joel Collins 2019-02-06 15:18:02 +00:00
parent 9253f1badf
commit 1436e58283
2 changed files with 94 additions and 19 deletions

View file

@ -1,5 +1,7 @@
Stream Object
Capture Object
=======================================================
By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.camera.capture.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory. The class also includes some convenience features such as automatically cleaning up captures marked as temporary, handling metadata files and file names, and generating image thumbnails. Often, the most recent capture wants to be store din memory, for applications that involve immediately processing the capture data in some way. Rather than manually keep track of the location of the capture data, you can use the CaptureObject ``data`` attribute, which automatically returns capture data from the fastest available location. Below are details of available methods and attributes.
.. automodule:: openflexure_microscope.camera.capture
:members:

View file

@ -12,11 +12,14 @@ import atexit
pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
thumbnail_size = (60, 60)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') #: str: Base path to store all captures
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') #: str: Base path to store all temporary captures (automatically emptied)
def clear_tmp():
"""
Removes all files in the ``TEMP_CAPTURE_PATH`` directory
"""
global TEMP_CAPTURE_PATH
logging.info("Clearing {}...".format(TEMP_CAPTURE_PATH))
@ -28,6 +31,14 @@ def clear_tmp():
def capture_from_dict(capture_dict):
"""
Creates an instance of CaptureObject from a dictionary of capture information.
This is used when reloading the API server, to restore captures created in the
previous session.
Args:
capture_dict (dict): Dictionary containing capture information
"""
capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture
# Populate capture parameters
@ -63,18 +74,18 @@ class CaptureObject(object):
"""Create a new StreamObject, to manage capture data."""
# Store a nice ID
self.id = uuid.uuid4().hex
self.id = uuid.uuid4().hex #: str: Unique capture ID
logging.info("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") #: str: Timestring of capture creation time
# Store file format
self.format = fmt
self.format = fmt #: str: Capture data format (e.g. jpeg, h264)
# Keep on disk after close by default
self.temporary = temporary
self.temporary = temporary #: bool: Mark the capture as temporary, to be deleted as the server closes, or as resources are required
# Explicitally state if capture should be written to a file, not a bytestream
self.write_to_file = write_to_file
self.write_to_file = write_to_file #: bool: Write capture data to disk immediately, rather than to memory initially (useful for video recordings)
# Create file name. Default to UUID
if not filename:
@ -87,10 +98,10 @@ class CaptureObject(object):
self.folder = folder
# Dictionary for storing custom metadata
self._metadata = {}
self._metadata = {} #: dict: Dictionary of custom metadata to be included in metadata file
# List for storing tags
self.tags = []
self.tags = [] #: list: List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
# Initialise the capture stream
self.initialise_capture(create_metadata_file=create_metadata_file)
@ -123,6 +134,14 @@ class CaptureObject(object):
self.close()
def initialise_capture(self, create_metadata_file=True):
"""
Initialise the capture object. Creates a file name and builds a file path,
creates an in-memory byte stream for capture data, and creates a metadata
file on disk.
Args:
create_metadata_file (bool): Skips creating a metadata file if False (useful for captures being processed but not stored)
"""
self.build_file_path(self.filename, self.folder)
# Byte bytestream properties
@ -146,8 +165,11 @@ class CaptureObject(object):
folder: str):
"""
Construct a full file path, based on filename, folder, and file format.
Defaults to UUID.
Args:
filename (str): Filename of capture
folder (str): Directory on disk to store capture file
"""
global TEMP_CAPTURE_PATH
@ -175,7 +197,12 @@ class CaptureObject(object):
logging.debug(self.filefolder)
def split_file_path(self, filepath):
"""Takes a full file path, and splits it into separated class properties."""
"""
Take a full file path, and split it into separated class properties.
Args:
filepath (str): String of the full file path, including file format extension
"""
self.filefolder, self.filename = os.path.split(
filepath) # Split the full file path into a folder and a filename
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension
@ -191,7 +218,7 @@ class CaptureObject(object):
@property
def stream_exists(self, auto_rewind=True) -> bool:
"""Check if BytesIO bytestream is empty."""
"""Check if capture data BytesIO bytestream exists in memory."""
if auto_rewind:
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.bytestream.getvalue(): # If data bytestream contains data
@ -203,7 +230,7 @@ class CaptureObject(object):
@property
def file_exists(self) -> bool:
"""Check if corresponding file exists."""
"""Check if capture data file exists on disk."""
if os.path.isfile(self.file):
return True
else:
@ -211,10 +238,22 @@ class CaptureObject(object):
# HANDLE TAGS
def put_tag(self, tag: str):
"""
Add a new tag to the ``tags`` list attribute.
Args:
tag (str): Tag to be added
"""
if not tag in self.tags:
self.tags.append(tag)
def delete_tag(self, tag: str):
"""
Remove a tag from the ``tags`` list attribute, if it exists.
Args:
tag (str): Tag to be removed
"""
if tag in self.tags:
self.tags = [new_tag for new_tag in self.tags if new_tag != tag]
@ -222,25 +261,43 @@ class CaptureObject(object):
@property
def metadata_file(self) -> str:
"""
Path to the associated on-disk metadata YAML file.
"""
return "{}.yaml".format(os.path.splitext(self.file)[0])
def put_metadata(self, data: dict) -> None:
"""
Merge metadata from a passed dictionary into the capture metadata, and saves.
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata.update(data)
self.save_metadata()
def save_metadata(self) -> None:
"""
Save metadata to on-disk metadata YAML file
"""
logging.debug("Writing metadata to file {}".format(self.metadata_file))
with open(self.metadata_file, 'w') as outfile:
yaml.dump(self.metadata, outfile, default_flow_style=False)
def delete_metadata(self) -> None:
"""
Delete on-disk metadata YAML file, if it exists
"""
if os.path.isfile(self.metadata_file):
logging.info("Deleting file {}".format(self.metadata_file))
os.remove(self.metadata_file)
@property
def metadata(self) -> dict:
# Create basic metadata dictionary
"""
Create basic metadata dictionary from basic capture data,
and any added custom metadata and tags.
"""
d = {'id': self.id, 'filename': self.filename, 'path': self.file, 'time': self.timestring,
'format': self.format, 'tags': self.tags, 'custom': self._metadata}
@ -249,22 +306,30 @@ class CaptureObject(object):
@property
def yaml(self) -> str:
"""
Return a string containing a YAML-formatted representation of the capture matadata
"""
return yaml.dump(self.metadata, default_flow_style=False)
@property
def exists(self) -> bool:
"""
Check if either an in-memory byte stream or on-disk file of capture data exists.
If False, the capture data cannot be obtained.
"""
return self.stream_exists or self.file_exists
@property
def state(self) -> dict:
"""Return dictionary of StreamObject properties."""
"""
Return a dictionary of objects full state, including metadata.
"""
# Create basic state dictionary
d = {'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata,
'metadata_path': self.metadata_file}
# Add metadata to state
# Check bytestream
if self.stream_exists:
d['bytestream'] = True
@ -281,7 +346,12 @@ class CaptureObject(object):
@property
def data(self) -> io.BytesIO:
"""Return a byte string of the capture data."""
"""
Return a byte string of the capture data.
If the capture data exists in-memory, this will be loaded. If the data only exists
on disk, this will automatically fall back to loading from disk.
"""
self.bytestream.seek(0) # Rewind the data bytes for reading
if self.stream_exists: # If data bytestream contains data
@ -308,6 +378,9 @@ class CaptureObject(object):
@property
def thumbnail(self) -> io.BytesIO:
"""
Returns a thumbnail of the capture data, for supported image formats.
"""
# If no thumbnail exists, try and make one
if not self.thumb_bytes:
logging.info("Building thumbnail")