Commit 78ce3a9a authored by Joel Collins's avatar Joel Collins
Browse files

Explicit output stream must be specified when capturing

parent 3f693322
Loading
Loading
Loading
Loading
+54 −11
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@ except ImportError:
    except ImportError:
        from _thread import get_ident

from .capture import StreamObject, BASE_CAPTURE_PATH
from .capture import CaptureObject, BASE_CAPTURE_PATH


def last_entry(object_list: list):
@@ -222,19 +222,62 @@ class BaseCamera(object):
        target_list.append(stream_object)
        return stream_object

    def new_image(self, stream_object, shunt_others=True):
    def shunt_captures(self, target_list: list):
        for obj in target_list:  # For each older capture
            obj.shunt()  # Shunt capture from memory to storage

    def new_image(
            self,
            write_to_file: bool=False,
            keep_on_disk: bool=True,
            filename: str=None,
            fmt: str='jpeg',
            shunt_others: bool=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):
        if not filename:
            filename = self.generate_basename(self.images)
            logging.debug(filename)

        output = CaptureObject(
            write_to_file=write_to_file,
            keep_on_disk=keep_on_disk,
            filename=filename,
            folder=self.paths['image'],
            fmt=fmt)

        self.shunt_captures(self.images)
        self.images.append(output)

        return output

    def new_video(
            self,
            write_to_file: bool=True,
            keep_on_disk: bool=True,
            filename: str=None,
            fmt: str='h264',
            quality: int=15,
            shunt_others: bool=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)

        if not filename:
            filename = self.generate_basename(self.videos)
            logging.debug(filename)

        output = CaptureObject(
            write_to_file=write_to_file,
            keep_on_disk=keep_on_disk,
            filename=filename,
            folder=self.paths['video'],
            fmt=fmt)

        self.shunt_captures(self.videos)
        self.videos.append(output)

        return output

    # INTELLIGENTLY GENERATE FILENAMES
    def generate_basename(self, obj_list: list) -> str:
+60 −51
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ thumbnail_size = (60, 60)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')

class StreamObject(object):
class CaptureObject(object):
    """
    StreamObject used to store and process capture data, and metadata.

@@ -26,6 +26,7 @@ class StreamObject(object):
            folder: str='',
            fmt: str='') -> None:
        """Create a new StreamObject, to manage capture data."""
        
        # Store a nice ID
        self.id = uuid.uuid4().hex
        logging.info("Created StreamObject {}".format(self.id))
@@ -37,24 +38,18 @@ class StreamObject(object):
        # Keep on disk after close by default
        self.keep_on_disk = keep_on_disk

        # Explicitally state if capture should be written to a file, not a bytestream
        self.write_to_file = write_to_file

        # Create file name. Default to UUID
        if not filename:
            filename = self.id
        self.filename = filename
        self.folder = folder
        self.build_file_path(self.filename, self.folder, self.format)

        # Byte stream properties
        self.stream = io.BytesIO()  # Byte stream that data will be written to
        self.filename = "{}.{}".format(filename, fmt)
        self.folder = folder

        # Set default write target
        self.write_to_file = write_to_file
        if not self.write_to_file:
            logging.debug("Target for {} set to 'stream'".format(self.id))
            self.target = self.stream
        else:
            logging.debug("Target for {} set to 'file'".format(self.id))
            self.target = self.file
        # Initialise the capture stream
        self.initialise_capture()

        # Log if created by context manager
        self.context_manager = False
@@ -67,14 +62,12 @@ class StreamObject(object):

    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))
        logging.debug("Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format(self.id))
        self.keep_on_disk = False  # Flag file to be removed on close.
        self.context_manager = True  # Used in metadata

        # Re-generate file name for temp file
        logging.info("Re-generating file name for temp file")
        self.build_file_path(self.filename, self.folder, self.format)
        logging.info("Rebuilding as a temporary capture...")
        self.initialise_capture()

        return self

@@ -83,11 +76,24 @@ class StreamObject(object):
        logging.info("Cleaning up {}".format(self.id))
        self.close()

    def initialise_capture(self):
        self.build_file_path(self.filename, self.folder)

        # Byte bytestream properties
        self.bytestream = io.BytesIO()  # Byte bytestream that data will be written to

        # Set default write target
        if not self.write_to_file:
            logging.debug("Target for {} set to 'bytestream'".format(self.id))
            self.stream = self.bytestream
        else:
            logging.debug("Target for {} set to 'file'".format(self.id))
            self.stream = self.file

    def build_file_path(
            self,
            filename: str,
            folder: str,
            fmt: str):
            folder: str):
        """
        Construct a full file path, based on filename, folder, and file format.

@@ -95,9 +101,7 @@ class StreamObject(object):
        """
        global TEMP_CAPTURE_PATH

        file_given_name = "{}.{}".format(filename, fmt)  # Given file name. May include a relative path

        self.file = os.path.join(folder, file_given_name)  # Full file name by joining given folder to given name
        self.file = os.path.join(folder, filename)  # Full file name by joining given folder to given name

        self.split_file_path(self.file)  # Split file path into folder, filename, and basename

@@ -116,6 +120,11 @@ class StreamObject(object):
        if not os.path.exists(self.filefolder):
            os.makedirs(self.filefolder)

        logging.debug(self.file)
        logging.debug(self.filename)
        logging.debug(self.basename)
        logging.debug(self.filefolder)

    def split_file_path(self, filepath):
        """Takes a full file path, and splits it into separated class properties."""
        self.filefolder, self.filename = os.path.split(filepath)  # Split the full file path into a folder and a filename
@@ -131,14 +140,14 @@ class StreamObject(object):

    @property
    def stream_exists(self, auto_rewind=True) -> bool:
        """Check if BytesIO stream is empty."""
        """Check if BytesIO bytestream 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
            self.bytestream.seek(0)  # Rewind the data bytes for reading
        if self.bytestream.getvalue():  # If data bytestream contains data
            self.bytestream.seek(0)  # Rewind the data bytes for reading
            return True
        else:
            self.stream.seek(0)  # Rewind the data bytes for reading
            self.bytestream.seek(0)  # Rewind the data bytes for reading
            return False

    @property
@@ -161,11 +170,11 @@ class StreamObject(object):
            'time': self.timestring
        }

        # Check stream
        # Check bytestream
        if self.stream_exists:
            d['stream'] = True
            d['bytestream'] = True
        else:
            d['stream'] = False
            d['bytestream'] = False

        # Combined availability of data
        if self.stream_exists or self.file_exists:
@@ -182,19 +191,19 @@ class StreamObject(object):
    @property
    def data(self) -> io.BytesIO:
        """Return a byte string of the capture data."""
        self.stream.seek(0)  # Rewind the data bytes for reading
        self.bytestream.seek(0)  # Rewind the data bytes for reading

        if self.stream_exists:  # If data stream contains data
            # Create a copy of the stream bytes
            data = io.BytesIO(self.stream.getbuffer())
        if self.stream_exists:  # If data bytestream contains data
            # Create a copy of the bytestream bytes
            data = io.BytesIO(self.bytestream.getbuffer())

        else:  # If data stream is empty
        else:  # If data bytestream is empty
            if self.file_exists:  # If data file exists
                logging.info("Opening from file {}".format(self.file))
                with open(self.file, 'rb') as f:
                    d = io.BytesIO(f.read())  # Load bytes from file
                d.seek(0)  # Rewind loaded stream
                # Create a copy of the stream bytes
                d.seek(0)  # Rewind loaded bytestream
                # Create a copy of the bytestream bytes
                data = io.BytesIO(d.getbuffer())
            else:
                data = None
@@ -226,27 +235,27 @@ class StreamObject(object):
        return data

    def load_file(self) -> bool:
        """Load data stored on disk to the in-memory stream."""
        """Load data stored on disk to the in-memory bytestream."""
        if self.file_exists:  # If data file exists
            with open(self.file, 'rb') as f:
                self.stream = io.BytesIO(f.read())  # Load bytes from file
            self.stream.seek(0)  # Rewind data bytes again
                self.bytestream = io.BytesIO(f.read())  # Load bytes from file
            self.bytestream.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 bytestream to a file."""
        if not self.keep_on_disk:  # If capture is currently temporary
            self.load_file()  # Load data from tmp file into stream, if tmp file exists
            self.load_file()  # Load data from tmp file into bytestream, if tmp file exists
            self.keep_on_disk = True  # Flag as kept on disk
            self.file = self.file_notmp  # Reset file path to non-temporary path
            self.split_file_path(self.file)  # Set split properties based on new path
            logging.info("Moved temporary file out to {}".format(self.file))

        if self.stream_exists:  # If there's a stream to save
        if self.stream_exists:  # If there's a bytestream to save
            with open(self.file, 'ab') as f:  # Load file as bytes
                logging.debug("Writing stream to file {}".format(self.file))
                logging.debug("Writing bytestream 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
@@ -254,8 +263,8 @@ class StreamObject(object):
            return False

    def delete_stream(self):
        """Clear the BytesIO stream of the StreamObject."""
        self.stream = io.BytesIO()
        """Clear the BytesIO bytestream of the StreamObject."""
        self.bytestream = io.BytesIO()

    def delete_file(self) -> bool:
        """If the StreamObject has been saved, delete the file."""
@@ -275,11 +284,11 @@ class StreamObject(object):
    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
            self.save_file()  # Save bytestream to disk, if it exists
        self.delete_stream()  # Delete the bytestream from memory

    def close(self):
        """Both clear the stream, and delete any associated on-disk data."""
        """Both clear the bytestream, and delete any associated on-disk data."""
        logging.info("Closing {}".format(self.id))
        self.delete_stream()
        if not self.keep_on_disk:
+25 −52
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ from typing import Tuple
# Threading
import threading

from .base import BaseCamera, StreamObject
from .base import BaseCamera, CaptureObject
# Richard's fix gain
from .set_picamera_gain import set_analog_gain, set_digital_gain

@@ -283,52 +283,40 @@ class StreamingCamera(BaseCamera):

    def start_recording(
            self,
            target=None,
            write_to_file: bool=True,
            filename: str=None,
            output,
            fmt: str='h264',
            quality: int=15):
        """Start recording.

        Start a new video recording, writing to a target object.
        Start a new video recording, writing to a output object.

        Args:
            target (str/BytesIO): Target object to write bytes to.
            output (CaptureObject): Output object to write data bytes to.
            write_to_file (bool/NoneType): Should the StreamObject write to a file?
            filename (str): Name of the stored file.  Defaults to timestamp.
            fmt (str): Format of the capture.

        Returns:
            target_object (str/BytesIO): Target object.
            output_object (str/BytesIO): Target object.

        """
        # Start recording method only if a current recording is not running
        if not self.state['record_active']:

            # If no target is specified, store to StreamingCamera
            if not target:
                # Create a new video and add to the video list
                target_obj = self.new_video(
                    StreamObject(
                        write_to_file=write_to_file,
                        filename=filename,
                        folder=self.paths['video'],
                        fmt=fmt)
                    )

            # If output is a StreamObject
            if isinstance(output, CaptureObject):
                # Lock the StreamObject while recording
                target_obj.lock()
                # Store to the StreamObject target
                target = target_obj.target

                output.lock()
                # Set target to capture stream
                output_stream = output.stream
            else:
                target_obj = target
                output_stream = output

            # Start the camera video recording on port 2
            logging.info("Recording to {}".format(target))
            logging.info("Recording to {}".format(output))

            self.camera.start_recording(
                target,
                output_stream,
                format=fmt,
                splitter_port=2,
                resize=self.config['video_resolution'],
@@ -337,7 +325,7 @@ class StreamingCamera(BaseCamera):
            # Update state dictionary
            self.state['record_active'] = True

            return target_obj
            return output

        else:
            print(
@@ -404,7 +392,7 @@ class StreamingCamera(BaseCamera):

    def capture(
            self,
            target=None,
            output,
            write_to_file: bool=False,
            keep_on_disk: bool=True,
            use_video_port: bool=False,
@@ -418,7 +406,7 @@ class StreamingCamera(BaseCamera):
        Target object can be overridden for development purposes.

        Args:
            target (str/BytesIO): Target object to write data bytes to.
            output (CaptureObject/str): Output object to write data bytes to.
            write_to_file (bool): Should the StreamObject write to a file, instead of BytesIO stream?
            use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
            filename (str): Name of the stored file. Defaults to timestamp.
@@ -426,29 +414,14 @@ class StreamingCamera(BaseCamera):
            resize ((int, int)): Resize the captured image.
        """

        # If no filename is specified, build a non-clashing one
        if not filename:
            filename = self.generate_basename(self.images)
            logging.debug(filename)

        # If no target is specified, store to StreamingCamera
        if not target:
            # TODO: Handle clashing file names here instead of in capture method.
            # Create a new image and add to the image list
            target_obj = self.new_image(
                StreamObject(
                    write_to_file=write_to_file,
                    keep_on_disk=keep_on_disk,
                    filename=filename,
                    folder=self.paths['image'],
                    fmt=fmt)
                )
            target = target_obj.target  # Store to the StreamObject BytesIO  

        # If output is a StreamObject
        if isinstance(output, CaptureObject):
            # Set target to capture stream
            output_stream = output.stream
        else:
            target_obj = target
            output_stream = output

        logging.info("Capturing to {}".format(target))
        logging.info("Capturing to {}".format(output))

        if not use_video_port:

@@ -456,7 +429,7 @@ class StreamingCamera(BaseCamera):
            self.pause_stream()

            self.camera.capture(
                target,
                output_stream,
                format=fmt,
                quality=100,
                resize=resize,
@@ -467,14 +440,14 @@ class StreamingCamera(BaseCamera):

        else:
            self.camera.capture(
                target,
                output_stream,
                format=fmt,
                quality=100,
                resize=resize,
                bayer=False,
                use_video_port=True)

        return target_obj
        return output

    def yuv(
            self,
+47 −38
Original line number Diff line number Diff line
#!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera, StreamObject
from openflexure_microscope.camera.pi import StreamingCamera, CaptureObject
import os
import io
import sys
@@ -39,47 +39,50 @@ class TestCaptureMethods(unittest.TestCase):
                camera.wait_for_camera()

                # Capture to a context (auto-deletes files when done)
                with camera.capture(
                        write_to_file=False,
                with camera.new_image(write_to_file=False) as output:

                    camera.capture(
                        output, 
                        use_video_port=use_video_port, 
                        resize=resize) as stream:
                        resize=resize
                    ) 

                    # Ensure file deletion fails and returns False
                    self.assertFalse(stream.delete_file())
                    self.assertFalse(output.delete_file())
                    # Ensure capture not stored to file
                    self.assertFalse(os.path.isfile(stream.file))
                    self.assertFalse(os.path.isfile(output.file))

                    # BEFORE DELETE: Ensure StreamObject 'stream' has
                    # a valid BytesIO object and byte string
                    self.assertTrue(isinstance(
                        stream.data,
                        output.data,
                        io.IOBase
                        ))
                    self.assertTrue(isinstance(
                        stream.binary,
                        output.binary,
                        (bytes, bytearray)
                        ))

                    # Save capture to file
                    stream.save_file()
                    output.save_file()
                    # Check file got saved
                    self.assertTrue(os.path.isfile(stream.file))
                    self.assertTrue(os.path.isfile(output.file))

                    # Delete file
                    stream.delete_file()
                    output.delete_file()
                    # Check file got deleted
                    self.assertFalse(os.path.isfile(stream.file))
                    self.assertFalse(os.path.isfile(output.file))

                    # AFTER DELETE: Ensure StreamObject 'stream' has
                    # a valid BytesIO object and byte string
                    self.assertTrue(isinstance(stream.data, io.IOBase))
                    self.assertTrue(isinstance(output.data, io.IOBase))
                    self.assertTrue(isinstance(
                        stream.binary,
                        output.binary,
                        (bytes, bytearray)
                        ))

                    # Create a PIL image from stream
                    image = Image.open(stream.data)
                    image = Image.open(output.data)

                    # Ensure a valid PIL image was created
                    self.assertTrue(isinstance(image, Image.Image))
@@ -107,26 +110,26 @@ class TestCaptureMethods(unittest.TestCase):
                camera.wait_for_camera()

                # Capture
                stream = camera.capture(
                    write_to_file=True,
                output = camera.capture(
                    camera.new_image(write_to_file=True),
                    use_video_port=use_video_port,
                    resize=resize)

                # Check file got saved
                self.assertTrue(os.path.isfile(stream.file))
                statinfo = os.stat(stream.file)
                self.assertTrue(os.path.isfile(output.file))
                statinfo = os.stat(output.file)
                self.assertTrue(statinfo.st_size > 0)

                # Ensure StreamObject 'stream' has
                # a valid BytesIO object and byte string
                self.assertTrue(isinstance(stream.data, io.IOBase))
                self.assertTrue(isinstance(stream.binary, (bytes, bytearray)))
                self.assertTrue(isinstance(output.data, io.IOBase))
                self.assertTrue(isinstance(output.binary, (bytes, bytearray)))

                # Ensure file deletion completes and returns True
                self.assertTrue(stream.delete_file())
                self.assertTrue(output.delete_file())

                # Check file got deleted
                self.assertFalse(os.path.isfile(stream.file))
                self.assertFalse(os.path.isfile(output.file))


class TestUnencodedMethods(unittest.TestCase):
@@ -204,12 +207,17 @@ class TestRecordMethods(unittest.TestCase):

        for write_to_file in [True, False, None]:

            logging.debug("\nWRITE TO FILE: {}".format(write_to_file))

            # Wait for camera
            camera.wait_for_camera()

            with camera.new_video(
                write_to_file=write_to_file
            ) as output:

                # Start recording
            with camera.start_recording(
                    write_to_file=write_to_file) as stream:
                camera.start_recording(output)

                # Record for 2 seconds
                time.sleep(2)
@@ -217,16 +225,16 @@ class TestRecordMethods(unittest.TestCase):
                camera.stop_recording()

                # Check stream
                self.assertTrue(isinstance(stream.data, io.IOBase))
                self.assertTrue(isinstance(stream.binary, (bytes, bytearray)))
                self.assertTrue(isinstance(output.data, io.IOBase))
                self.assertTrue(isinstance(output.binary, (bytes, bytearray)))

                # Check file
                if write_to_file:
                    statinfo = os.stat(stream.file)
                    statinfo = os.stat(output.file)
                    self.assertTrue(statinfo.st_size > 0)

                # Log path
                temp_path = stream.file
                temp_path = output.file

            # Check file got deleted on __exit__
            self.assertFalse(os.path.isfile(temp_path))
@@ -241,21 +249,22 @@ class TestRecordMethods(unittest.TestCase):
        camera.wait_for_camera()

        # Start recording
        stream = camera.start_recording()
        output = camera.start_recording(camera.new_video())

        # Record for 2 seconds
        time.sleep(2)
        # Stop recording
        camera.stop_recording()

        # Check file
        statinfo = os.stat(stream.file)
        statinfo = os.stat(output.file)
        self.assertTrue(statinfo.st_size > 0)

        # Ensure file deletion completes and returns True
        self.assertTrue(stream.delete_file())
        self.assertTrue(output.delete_file())

        # Check file got deleted
        self.assertFalse(os.path.isfile(stream.file))
        self.assertFalse(os.path.isfile(output.file))


class TestThreadStarting(unittest.TestCase):
@@ -285,10 +294,10 @@ if __name__ == '__main__':
    camera = StreamingCamera()

    suites = [
        #unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
        unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
        unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
        #unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
        #unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
        unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
        unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
    ]

    alltests = unittest.TestSuite(suites)