Source code for fissa.extraction

"""
DataHandler classes to handle and manipulate image and ROI objects.

Authors:
    - Sander W Keemink <swkeemink@scimail.eu>
    - Scott C Lowe <scott.code.lowe@gmail.com>
"""

import abc
import warnings

import numpy as np
import six
import tifffile
from past.builtins import basestring
from PIL import Image, ImageSequence

from . import roitools

try:
    ABC = abc.ABC  # Python >= 3.4
except (AttributeError, NameError):
    ABC = object


[docs]@six.add_metaclass(abc.ABCMeta) # Python 2.7 backward compatibility class DataHandlerAbstract(ABC): """ Abstract class for a data handler. Note ---- - The `data` input into :meth:`getmean`, :meth:`rois2masks`, and :meth:`extracttraces` must be the same format as the output to :meth:`image2array`. - The `masks` input into :meth:`extracttraces` must be the same format as the output of :meth:`rois2masks`. See Also -------- DataHandlerTifffile, DataHandlerPillow """ def __repr__(self): return "{}.{}()".format(__name__, self.__class__.__name__)
[docs] @abc.abstractmethod def image2array(image): """ Load data (from a path) as an array, or similar internal structure. Parameters ---------- image : image_type Some handle to, or representation of, the raw imagery data. Returns ------- data : data_type Internal representation of the images which will be used by all the other methods in this class. """ raise NotImplementedError()
[docs] @abc.abstractmethod def getmean(data): """ Determine the mean image across all frames. Must return a 2D :class:`numpy.ndarray`. Parameters ---------- data : data_type The same object as returned by :meth:`image2array`. Returns ------- mean : numpy.ndarray Mean image as a 2D, y-by-x, array. """ raise NotImplementedError()
[docs] @abc.abstractmethod def get_frame_size(data): """ Determine the shape of each frame within the recording. Parameters ---------- data : data_type The same object as returned by :meth:`image2array`. Returns ------- shape : tuple of ints The 2D, y-by-x, shape of each frame in the movie. """ raise NotImplementedError()
[docs] @classmethod def rois2masks(cls, rois, data): """ Convert ROIs into a collection of binary masks. Parameters ---------- rois : str or :term:`list` of :term:`array_like` Either a string containing a path to an ImageJ roi zip file, or a list of arrays encoding polygons, or list of binary arrays representing masks. data : data_type The same object as returned by :meth:`image2array`. Returns ------- masks : mask_type Masks, in a format accepted by :meth:`extracttraces`. See Also -------- fissa.roitools.getmasks, fissa.roitools.readrois """ return roitools.rois2masks(rois, cls.get_frame_size(data))
[docs] @abc.abstractmethod def extracttraces(data, masks): """ Extract from data the average signal within each mask, across time. Must return a 2D :class:`numpy.ndarray`. Parameters ---------- data : data_type The same object as returned by :meth:`image2array`. masks : mask_type The same object as returned by :meth:`rois2masks`. Returns ------- traces : numpy.ndarray Trace for each mask, shaped ``(len(masks), n_frames)``. """ raise NotImplementedError()
[docs]class DataHandlerTifffile(DataHandlerAbstract): """ Extract data from TIFF images using tifffile. """
[docs] @staticmethod def image2array(image): """ Load a TIFF image from disk. Parameters ---------- image : str or :term:`array_like` shaped (time, height, width) Either a path to a TIFF file, or :term:`array_like` data. Returns ------- numpy.ndarray A 3D array containing the data, with dimensions corresponding to ``(frames, y_coordinate, x_coordinate)``. """ if not isinstance(image, basestring): return np.asarray(image) with tifffile.TiffFile(image) as tif: frames = [] n_pages = len(tif.pages) for page in tif.pages: page = page.asarray() if page.ndim < 2: raise EnvironmentError( "TIFF {} has pages with {} dimensions (page shaped {})." " Pages must have at least 2 dimensions".format( image, page.ndim, page.shape ) ) if ( n_pages > 1 and page.ndim > 2 and (np.array(page.shape[:-2]) > 1).sum() > 0 ): warnings.warn( "Multipage TIFF {} with {} pages has at least one page" " with {} dimensions (page shaped {})." " All dimensions before the final two (height and" " width) will be treated as time-like and flattened." "".format(image, n_pages, page.ndim, page.shape) ) elif page.ndim > 3 and (np.array(page.shape[:-2]) > 1).sum() > 1: warnings.warn( "TIFF {} has at least one page with {} dimensions" " (page shaped {})." " All dimensions before the final two (height and" " width) will be treated as time-like and flattened." "".format(image, page.ndim, page.shape) ) shp = [-1] + list(page.shape[-2:]) frames.append(page.reshape(shp)) return np.concatenate(frames, axis=0)
[docs] @staticmethod def getmean(data): """ Determine the mean image across all frames. Parameters ---------- data : :term:`array_like` Data array as made by :meth:`image2array`, shaped ``(frames, y, x)``. Returns ------- numpy.ndarray y by x array for the mean values """ return data.mean(axis=0, dtype=np.float64)
[docs] @staticmethod def get_frame_size(data): """ Determine the shape of each frame within the recording. Parameters ---------- data : data_type The same object as returned by :meth:`image2array`. Returns ------- shape : tuple of ints The 2D, y-by-x, shape of each frame in the movie. """ return data.shape[-2:]
[docs] @staticmethod def extracttraces(data, masks): """ Extract a temporal trace for each spatial mask. Parameters ---------- data : :term:`array_like` Data array as made by :meth:`image2array`, shaped ``(frames, y, x)``. masks : :class:`list` of :term:`array_like` List of binary arrays. Returns ------- traces : numpy.ndarray Trace for each mask, shaped ``(len(masks), n_frames)``. """ # get the number rois and frames nrois = len(masks) nframes = data.shape[0] # predefine output data out = np.zeros((nrois, nframes)) # loop over masks for i in range(nrois): # for masks # get mean data from mask out[i, :] = data[:, masks[i]].mean(axis=1, dtype=np.float64) return out
[docs]class DataHandlerTifffileLazy(DataHandlerAbstract): """ Extract data from TIFF images using tifffile, with lazy loading. """
[docs] @staticmethod def image2array(image): """ Load a TIFF image from disk. Parameters ---------- image : str A path to a TIFF file. Returns ------- data : tifffile.TiffFile Open tifffile.TiffFile object. """ return tifffile.TiffFile(image)
[docs] @staticmethod def getmean(data): """ Determine the mean image across all frames. Parameters ---------- data : tifffile.TiffFile Open tifffile.TiffFile object. Returns ------- numpy.ndarray y by x array for the mean values """ # We don't load the entire image into memory at once, because # it is likely to be rather large. memory = None n_frames = 0 n_pages = len(data.pages) for page in data.pages: page = page.asarray() if ( n_pages > 1 and page.ndim > 2 and (np.array(page.shape[:-2]) > 1).sum() > 0 ): warnings.warn( "Multipage TIFF {} with {} pages has at least one page" " with {} dimensions (page shaped {})." " All dimensions before the final two (height and" " width) will be treated as time-like and flattened." "".format("", n_pages, page.ndim, page.shape) ) elif page.ndim > 3 and (np.array(page.shape[:-2]) > 1).sum() > 1: warnings.warn( "TIFF {} has at least one page with {} dimensions" " (page shaped {})." " All dimensions before the final two (height and" " width) will be treated as time-like and flattened." "".format("", page.ndim, page.shape) ) shp = [-1] + list(page.shape[-2:]) page = page.reshape(shp) if memory is None: # Initialise holding array with zeros, now we know the shape # of the image frames memory = np.zeros(page.shape[-2:], dtype=np.float64) memory += np.mean(page, dtype=np.float64, axis=0) * page.shape[0] n_frames += page.shape[0] return memory / n_frames
[docs] @staticmethod def get_frame_size(data): """ Determine the shape of each frame within the recording. Parameters ---------- data : data_type The same object as returned by :meth:`image2array`. Returns ------- shape : tuple of ints The 2D, y-by-x, shape of each frame in the movie. """ return data.pages[0].shape[-2:]
[docs] @staticmethod def extracttraces(data, masks): """ Extract a temporal trace for each spatial mask. Parameters ---------- data : tifffile.TiffFile Open tifffile.TiffFile object. masks : list of array_like List of binary arrays. Returns ------- traces : numpy.ndarray Trace for each mask, shaped ``(len(masks), n_frames)``. """ # Get the number rois nrois = len(masks) # Initialise output as a list, because we don't know how many frames # there will be out = [] # For each frame, get the data for page in data.pages: page = page.asarray() shp = [-1] + list(page.shape[-2:]) page = page.reshape(shp) page_traces = np.zeros((nrois, page.shape[0]), dtype=np.float64) for i in range(nrois): # Get mean data from mask page_traces[i, :] = np.mean( page[..., masks[i]], dtype=np.float64, axis=-1, ) out.append(page_traces) out = np.concatenate(out, axis=-1) return out
[docs]class DataHandlerPillow(DataHandlerAbstract): """ Extract data from TIFF images frame-by-frame using Pillow (:class:`PIL.Image`). Slower, but less memory-intensive than :class:`DataHandlerTifffile`. """
[docs] @staticmethod def image2array(image): """ Open an image file as a :class:`PIL.Image` instance. Parameters ---------- image : str or file A filename (string) of a TIFF image file, a :class:`pathlib.Path` object, or a file object. Returns ------- data : PIL.Image Handle from which frames can be loaded. """ return Image.open(image)
[docs] @staticmethod def getmean(data): """ Determine the mean image across all frames. Parameters ---------- data : PIL.Image An open :class:`PIL.Image` handle to a multi-frame TIFF image. Returns ------- mean : numpy.ndarray y-by-x array for the mean values. """ # We don't load the entire image into memory at once, because # it is likely to be rather large. # Initialise holding array with zeros avg = np.zeros(data.size[::-1], dtype=np.float64) # Make sure we seek to the first frame before iterating. This is # because the Iterator outputs the value for the current frame for # `img` first, due to a bug in Pillow<=3.1. data.seek(0) # Loop over all frames and sum the pixel intensities together for frame in ImageSequence.Iterator(data): avg += np.asarray(frame) # Divide by number of frames to find the average avg /= data.n_frames return avg
[docs] @staticmethod def get_frame_size(data): """ Determine the shape of each frame within the recording. Parameters ---------- data : PIL.Image An open :class:`PIL.Image` handle to a multi-frame TIFF image. Returns ------- shape : tuple of ints The 2D, y-by-x, shape of each frame in the movie. """ return data.size[::-1]
[docs] @staticmethod def extracttraces(data, masks): """ Extract the average signal within each mask across the data. Parameters ---------- data : PIL.Image An open :class:`PIL.Image` handle to a multi-frame TIFF image. masks : list of :term:`array_like` List of binary arrays. Returns ------- traces : numpy.ndarray Trace for each mask, shaped ``(len(masks), n_frames)``. """ # get the number rois nrois = len(masks) # get number of frames, and start at zeros data.seek(0) nframes = data.n_frames # predefine array with the data out = np.zeros((nrois, nframes)) # for each frame, get the data for f in range(nframes): # set frame data.seek(f) # make numpy array curframe = np.asarray(data) # loop over masks for i in range(nrois): # get mean data from mask out[i, f] = np.mean(curframe[masks[i]], dtype=np.float64) return out