Improed CaptureObject documentation
This commit is contained in:
parent
9253f1badf
commit
1436e58283
2 changed files with 94 additions and 19 deletions
|
|
@ -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:
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue