Pipelines

Videos loaded by pims (FramesSequence objects) are like lists of numpy arrays. Unlike Python lists of arrays they are “lazy”, they only load the data from the harddrive when it is necessary.

In order to modify a FramesSequence, for instance to convert RGB color videos to grayscale, one could load all the video frames in memory and do the conversion. This however costs a lot of time and memory, and for very large videos this is just not feasible. To solve this problem, PIMS uses so-called pipeline decorators from a sister project called slicerator. A pipeline-decorated function is only evaluated when needed, so that the underlying video data is only accessed one element at a time.

Note

This supersedes the process_func and as_grey reader keyword arguments starting from PIMS v0.4

Conversion to greyscale

Say we want to convert an RGB video to greyscale. A pipeline to do this is already provided as pims.as_grey, but it is also easy to make our own. We define a function as follows and decorate it with @pipeline to turn it into a pipeline:

@pims.pipeline
def as_grey(frame):
    red = frame[:, :, 0]
    green = frame[:, :, 1]
    blue = frame[:, :, 2]
    return 0.2125 * red + 0.7154 * green + 0.0721 * blue

The behavior of as_grey is unchanged if it is used on a single frame:

In [1]: frame = video[0]

In [2]: print(frame.shape)   # the shape of the example video is RGB
(256, 256, 3)

In [3]: processed_frame = as_grey(frame)

In [4]: print(processed_frame.shape)  # the converted frame is indeed greyscale
(256, 256)

However, the @pipeline decorator enables lazy evaluation of full videos:

In [5]: processed_video = as_grey(video)  # this would not be possible without @pipeline

# nothing has been converted yet!
In [6]: processed_frame = processed_video[0]  # now the conversion takes place

In [7]: print(processed_frame.shape)
(256, 256)

This means that the modified video can be used exactly as you would use the original one. In most cases, it will look as though you are accessing a grayscale video file, even though the file on disk is still in color. Please keep in mind that these simple pipelines do not change the reader properties, such as video.frame_shape.

Propagating metadata properly through pipelines is partly implemented, but currently still experimental. For a detailed description of this tricky point, please consult this discussion on GitHub.

Cropping

Along with the built-in pims.as_grey pipeline that saves you from typing out the previous example, there’s also a pims.process.crop pipeline that _does_ change frame_shape. This example takes the video we had converted to grayscale in the previous example, and removes 15 pixels from the left side of each image:

In [8]: print(video.frame_shape)
(256, 256, 3)

# Because this is a color video, we need 3 pairs of cropping parameters
In [9]: cropped_video = pims.process.crop(video, ((0, 0), (15, 0), (0, 0)) )

In [10]: print(cropped_video.frame_shape)
(256, 241, 3)

In [11]: cropped_frame = cropped_video[0]  # now the cropping happens

In [12]: print(cropped_frame.shape)
(256, 241, 3)

Converting existing functions to a pipeline

We are now going to do the same greyscale conversion as above, but using an existing function from skimage:

In [13]: from skimage.color import rgb2gray

In [14]: rgb2gray_pipeline = pims.pipeline(rgb2gray)

In [15]: processed_video = rgb2gray_pipeline(video)

In [16]: processed_frame = processed_video[0]

In [17]: print(processed_frame.shape)
(256, 256)

Any function that takes a single frame and returns a single frame can be converted into a pipeline in this way.

Dtype conversion using lambda functions

Note

This supersedes the dtype reader keyword argument starting from PIMS v0.4

We are now going to convert the data type of a video to float using an unnamed lambda function in a single line:

In [18]: processed_video = pims.pipeline(lambda x: x.astype(float))(video)

In [19]: processed_frame = processed_video[0]

In [20]: print(processed_frame.shape)
(256, 256, 3)