Source code for jicbioimage.core.image

"""Module for managing and accessing images."""

import os
import json
import base64
import tempfile
import math
import re
import shutil

import numpy as np
import scipy.ndimage
import skimage.io

from jicbioimage.core.util.array import normalise


def _sorted_listdir(directory):
    """Return list of files sorted in the way humans expect.

    :param directory: path to directory
    :returns: sorted list of file names
    """

    def _sorted_nicely(l):
        """Return list sorted in the way that humans expect.

        :param l: iterable to be sorted
        :returns: sorted list
        """
        convert = lambda text: int(text) if text.isdigit() else text
        sort_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
        return sorted(l, key=sort_key)

    fpaths = os.listdir(directory)
    return _sorted_nicely(fpaths)


class _TemporaryFilePath(object):
    """Temporary file path context manager."""
    def __init__(self, suffix):
        self.suffix = suffix

    def __enter__(self):
        tmp_file = tempfile.NamedTemporaryFile(suffix=self.suffix,
                                               delete=False)
        self.fpath = tmp_file.name
        tmp_file.close()
        return self

    def __exit__(self, type, value, tb):
        os.unlink(self.fpath)


class _BaseImage(np.ndarray):
    """Private image base class with png repr functionality.

    Needed to split out the png functionality from the
    :class:`jicbioimage.core.image.Image` class in order to be able to re-use
    it in :class:`jicbioimage.illustrate.Canvas`.

    This base class will remain private until we can find a suitable name for
    it.
    """

    def __repr__(self):
        obj_type = self.__class__.__name__
        pos = hex(id(self))
        return "<{} object at {}, dtype={}>".format(obj_type, pos, self.dtype)

    def png(self, width=None):
        """Return png string of image.

        :param width: integer specifying the desired width
        :returns: png as a string
        """
        skimage.io.use_plugin('freeimage')

        def resize(im, width):
            x, y = im.shape[:2]
            f = float(width) / float(x)
            scale_factors = [1.0 for i in range(len(im.shape))]
            scale_factors[0] = f
            scale_factors[1] = f
            ar = scipy.ndimage.zoom(im, scale_factors, order=0)
            return Image.from_array(ar, log_in_history=False)

        safe_range_im = self

        if self.dtype != np.uint8:
            safe_range_im = 255 * normalise(self)

        if width is not None:
            safe_range_im = resize(safe_range_im, width)

        with _TemporaryFilePath(suffix='.png') as tmp:
            safe_range_im_uint8 = safe_range_im.astype(np.uint8)
            skimage.io.imsave(tmp.fpath, safe_range_im_uint8, "freeimage")
            with open(tmp.fpath, 'rb') as fh:
                return fh.read()

    def _repr_png_(self):
        """Return image as png string.

        Used by IPython qtconsole/notebook to display images.
        """
        return self.png()

    def write(self, name):
        """Write image to disk.

        :name: name of output file without extension
        """
        fpath = name + ".png"
        with open(fpath, "wb") as fh:
            fh.write(self.png())


[docs]class History(list): """Class for storing the provenance of an image.""" def __init__(self, creation=None): self.creation = creation def extend(self, other): self.creation = other.creation super(History, self).extend(other)
[docs] class Event(object): """An event in the history of an image.""" def __init__(self, function, args, kwargs): self.function = function self.args = args self.kwargs = kwargs def __repr__(self): return str(self) def __str__(self): def quote_strings(value): if isinstance(value, str): return "'{}'".format(value) return value def stringify(value): return str(quote_strings(value)) args = [stringify(v) for v in self.args] kwargs = ["{}={}".format(k, stringify(v)) for k, v in self.kwargs.items()] info = ["image"] + args + kwargs info = ", ".join(info) return "<History.Event({}({}))>".format(self.function.__name__, info)
[docs] def add_event(self, function, args, kwargs): """Return event added to the history.""" event = History.Event(function, args, kwargs) self.append(event) return event
class _BaseImageWithHistory(_BaseImage): """Private image base class that adds history on creation.""" @classmethod def from_array(cls, array, name=None, log_in_history=True): """Return :class:`jicbioimage.core.image.Image` instance from an array. :param array: :class:`numpy.ndarray` :param name: name of the image :param log_in_history: whether or not to log the creation event in the image's history :returns: :class:`jicbioimage.core.image.Image` """ image = array.view(cls) class_name = cls.__name__ creation = 'Created {} from array'.format(class_name) if name: creation = '{} as {}'.format(creation, name) if log_in_history: image.history.creation = creation return image def __new__(subtype, shape, dtype=np.uint8, buffer=None, offset=0, strides=None, order=None, name=None, log_in_history=True): obj = np.ndarray.__new__(subtype, shape, dtype, buffer, offset, strides, order) obj.name = name obj.history = History() return obj def __init__(self, shape, dtype=np.uint8, buffer=None, offset=0, strides=None, order=None, name=None, log_in_history=True): class_name = self.__class__.__name__ creation = 'Instantiated {} from shape {}'.format(class_name, shape) if name: creation = '{} as {}'.format(creation, name) if log_in_history: self.history.creation = creation def __array_finalize__(self, obj): if obj is None: return self.name = getattr(obj, 'name', None) self.history = getattr(obj, 'history', History())
[docs]class Image(_BaseImageWithHistory): """Image class.""" @classmethod
[docs] def from_file(cls, fpath, name=None, log_in_history=True): """Return :class:`jicbioimage.core.image.Image` instance from a file. :param fpath: path to the image file :param name: name of the image :param log_in_history: whether or not to log the creation event in the image's history :returns: :class:`jicbioimage.core.image.Image` """ skimage.io.use_plugin('freeimage') ar = skimage.io.imread(fpath, plugin="freeimage") # Create a :class:`jicbioimage.core.image.Image` instance. image = Image.from_array(ar, name) # Reset history, as image is created from file not array. image.history = History() class_name = cls.__name__ creation = 'Created {} from {}'.format(class_name, fpath) if name: creation = '{} as {}'.format(creation, name) if log_in_history: image.history.creation = creation return image
[docs]class Image3D(_BaseImageWithHistory): """Image3D class; in other words a 3D stack.""" @staticmethod def _num_digits(zdim): return int(math.floor(math.log10(abs(zdim))) + 1) @classmethod
[docs] def from_directory(cls, directory): """Return :class:`jicbioimage.core.image.Image3D` from directory. :param directory: name of input directory :returns: :class:`jicbioimage.core.image.Image3D` """ skimage.io.use_plugin('freeimage') def is_image_fname(fname): "Return True if fname is '.png', '.tif' or '.tiff'.""" image_exts = set([".png", ".tif", ".tiff"]) base, ext = os.path.splitext(fname) return ext in image_exts fnames = [fn for fn in _sorted_listdir(directory) if is_image_fname(fn)] fpaths = [os.path.join(directory, fn) for fn in fnames] images = [skimage.io.imread(fp, plugin="freeimage") for fp in fpaths] stack = np.dstack(images) return cls.from_array(stack)
[docs] def to_directory(self, directory): """Write slices from 3D image to directory. """ if not os.path.isdir(directory): os.mkdir(directory) xdim, ydim, zdim = self.shape num_digits = Image3D._num_digits(zdim-1) ar = normalise(self) * 255 ar = ar.astype(np.uint8) for z in range(zdim): num = str(z).zfill(num_digits) fname = "z{}.png".format(num) fpath = os.path.join(directory, fname) skimage.io.imsave(fpath, ar[:, :, z], "freeimage")
[docs] def write(self, name): """Write slices from 3D image to disk. :param name: name of output directory """ dirname = name + ".stack" if os.path.isdir(dirname): shutil.rmtree(dirname) os.mkdir(dirname) self.to_directory(dirname)
[docs]class ProxyImage(object): """Lightweight image class.""" def __init__(self, fpath, metadata={}): self.fpath = fpath for key, value in metadata.items(): self.__setattr__(key, value) def __repr__(self): return "<ProxyImage object at {}>".format(hex(id(self))) def __info_html_table__(self, index): table = "<table><tr><th>Index</th><td>{}</td></tr></table>" return table.format(index) @property def image(self): """Underlying :class:`jicbioimage.core.image.Image` instance.""" return Image.from_file(self.fpath) def _repr_png_(self): """Return image as png string. Used by IPython qtconsole/notebook to display images. """ return self.image.png()
[docs]class MicroscopyImage(ProxyImage): """Lightweight image class with microscopy meta data.""" def __repr__(self): return "<MicroscopyImage(s={}, c={}, z={}, t={}) object at {}>".format( self.series, self.channel, self.zslice, self.timepoint, hex(id(self))) def __info_html_table__(self, index): return """ <table> <tr> <th>Index</th> <th>Series</th> <th>Channel</th> <th>Z-slice</th> <th>Time point</th> </tr> <tr> <td>{}</td> <td>{}</td> <td>{}</td> <td>{}</td> <td>{}</td> </tr> </table> """.format(index, self.series, self.channel, self.zslice, self.timepoint)
[docs] def is_me(self, s, c, z, t): """Return True if arguments match my meta data. :param s: series :param c: channel :param z: zslice :param t: timepoint :returns: :class:`bool` """ if (self.series == s and self.channel == c and self.zslice == z and self.timepoint == t): return True return False
[docs] def in_zstack(self, s, c, t): """Return True if I am in the zstack. :param s: series :param c: channel :param t: timepoint :returns: :class:`bool` """ if (self.series == s and self.channel == c and self.timepoint == t): return True return False
[docs]class ImageCollection(list): """Class for storing related images.""" def __init__(self, fpath=None): if fpath is not None: self.parse_manifest(fpath)
[docs] def proxy_image(self, index=0): """Return a :class:`jicbioimage.core.image.ProxyImage` instance. :param index: list index :returns: :class:`jicbioimage.core.image.ProxyImage` """ return self[index]
[docs] def image(self, index=0): """Return image as a :class:`jicbioimage.core.image.Image`. :param index: list index :returns: :class:`jicbioimage.core.image.Image` """ return self.proxy_image(index=index).image
[docs] def parse_manifest(self, fpath): """Parse manifest file to build up the collection of images. :param fpath: path to the manifest file :raises: RuntimeError """ directory = os.path.dirname(fpath) with open(fpath, 'r') as fh: for entry in json.load(fh): # Every entry of a manifest file needs to have a "filename" # attribute. It is the only requirement so we check for it in a # strict fashion. if "filename" not in entry: raise(RuntimeError( 'Entries in {} need to have "filename"'.format(fpath))) filename = entry.pop("filename") fpath = os.path.join(directory, os.path.basename(filename)) proxy_image = None if isinstance(self, MicroscopyCollection): proxy_image = MicroscopyImage(fpath, entry) else: proxy_image = ProxyImage(fpath, entry) self.append(proxy_image)
def _repr_html_(self): """Return image collection as html. Used by IPython notebook to display the image collection. """ DIV_HTML = '''<div style="float: left; padding: 2px;" >{}</div>''' CONTENT_HTML = '''<p>{}</p> <img style="margin-left: auto; margin-right: auto;" src="data:image/png;base64,{}" /> ''' lines = [] for i, proxy_image in enumerate(self): png = proxy_image.image.png(width=300) b64_png = base64.b64encode(png).decode('utf-8') l = DIV_HTML.format( CONTENT_HTML.format( proxy_image.__info_html_table__(i), b64_png ) ) lines.append(l) return '\n'.join(lines)
[docs]class MicroscopyCollection(ImageCollection): """ Collection of :class:`jicbioimage.core.image.MicroscopyImage` instances. """ @property def series(self): """Return list of series in the collection.""" return sorted(list(set([mi.series for mi in self])))
[docs] def channels(self, s=0): """Return list of channels in the collection. :param s: series :returns: list of channel identifiers """ return sorted(list(set([mi.channel for mi in self if mi.series == s])))
[docs] def zslices(self, s=0): """Return list of z-slices in the collection. :param s: series :returns: list of zslice identifiers """ return sorted(list(set([mi.zslice for mi in self if mi.series == s])))
[docs] def timepoints(self, s=0): """Return list of time points in the collection. :param s: series :returns: list of time point identifiers """ return sorted(list(set([mi.timepoint for mi in self if mi.series == s])))
[docs] def proxy_image(self, s=0, c=0, z=0, t=0): """Return a :class:`jicbioimage.core.image.MicroscopyImage` instance. :param s: series :param c: channel :param z: zslice :param t: timepoint :returns: :class:`jicbioimage.core.image.MicroscopyImage` """ for proxy_image in self: if proxy_image.is_me(s=s, c=c, z=z, t=t): return proxy_image
[docs] def zstack_proxy_iterator(self, s=0, c=0, t=0): """ Return zstack :class:`jicbioimage.core.image.ProxyImage` iterator. :param s: series :param c: channel :param t: timepoint :returns: zstack as a :class:`jicbioimage.core.image.ProxyImage` iterator """ for proxy_image in self: if proxy_image.in_zstack(s=s, c=c, t=t): yield proxy_image
[docs] def zstack_array(self, s=0, c=0, t=0): """Return zstack as a :class:`numpy.ndarray`. :param s: series :param c: channel :param t: timepoint :returns: zstack as a :class:`numpy.ndarray` """ zstack = [x.image for x in self.zstack_proxy_iterator(s=s, c=c, t=t)] return np.dstack(zstack)
[docs] def zstack(self, s=0, c=0, t=0): """Return zstack as a :class:`jicbioimage.core.image.Image3D`. :param s: series :param c: channel :param t: timepoint :returns: zstack as a :class:`jicbioimage.core.image.Image3D` """ return Image3D.from_array(self.zstack_array(s=s, c=c, t=t))
[docs] def image(self, s=0, c=0, z=0, t=0): """Return image as a :class:`jicbioimage.core.image.Image`. :param s: series :param c: channel :param z: zslice :param t: timepoint :returns: :class:`jicbioimage.core.image.Image` """ return self.proxy_image(s=s, c=c, z=z, t=t).image