diff --git a/.travis.yml b/.travis.yml index e0f289f7..d51ad3e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,11 @@ sudo: false matrix: include: - python: "2.7" - env: DEPS="numpy=1.8* slicerator tifffile" BUILD_DOCS=false + env: DEPS="numpy=1.8* slicerator pillow tifffile jinja2" DEPSPIP="imageio" BUILD_DOCS=false - python: "2.7" env: DEPS="numpy slicerator scipy pillow matplotlib scikit-image jinja2 av libtiff tifffile jpype1" DEPSPIP="moviepy imageio" BUILD_DOCS=false - python: "3.6" - env: DEPS="numpy slicerator tifffile" BUILD_DOCS=false + env: DEPS="numpy slicerator pillow tifffile jinja2" DEPSPIP="imageio" BUILD_DOCS=false - python: "3.6" env: DEPS="numpy slicerator scipy pillow matplotlib scikit-image jinja2 av tifffile libtiff jpype1 ipython sphinx sphinx_rtd_theme numpydoc" DEPSPIP="moviepy imageio" BUILD_DOCS=true diff --git a/pims/api.py b/pims/api.py index c6c02dab..087db0cd 100644 --- a/pims/api.py +++ b/pims/api.py @@ -7,6 +7,7 @@ from pims.display import (export, play, scrollable_stack, to_rgb, normalize, plot_to_frame, plots_to_frame) from itertools import chain +from functools import wraps import six import glob @@ -14,12 +15,13 @@ from warnings import warn # has to be here for API stuff +from pims.base_frames import WrapImageIOReader from pims.image_sequence import ImageSequence, ImageSequenceND, ReaderSequence # noqa from pims.image_reader import ImageReader, ImageReaderND # noqa from .cine import Cine # noqa from .norpix_reader import NorpixSeq # noqa -from pims.tiff_stack import TiffStack_tifffile # noqa from .spe_stack import SpeStack +from imageio import formats, get_reader def not_available(requirement): @@ -28,6 +30,17 @@ def raiser(*args, **kwargs): "This reader requires {0}.".format(requirement)) return raiser + +def register_fmt(reader, name, description, extensions=None, modes=None): + """Registers a Format with the format manager. Returns a reader + for backwards compatibility.""" + formats.add_format(reader(name, description, extensions, modes)) + @wraps(reader) + def wrapper(filename, **kwargs): + return WrapImageIOReader(get_reader(filename, name, **kwargs)) + return wrapper + + if export is None: export = not_available("PyAV or MoviePy") @@ -75,12 +88,17 @@ def raiser(*args, **kwargs): Video = not_available("PyAV, MoviePy, or ImageIO") import pims.tiff_stack -from pims.tiff_stack import (TiffStack_pil, TiffStack_libtiff, - TiffStack_tifffile) +from pims.tiff_stack import TiffStack_pil, TiffStack_libtiff, \ + FormatTiffStack_tifffile # First, check if each individual class is available # and drop in placeholders as needed. if not pims.tiff_stack.tifffile_available(): - TiffStack_tiffile = not_available("tifffile") + TiffStack_tifffile = not_available("tifffile") +else: + TiffStack_tifffile = register_fmt(FormatTiffStack_tifffile, + 'TIFF_tifffile', + 'Reads TIFF files through tifffile.py.', + 'tif tiff lsm stk', 'iIvV') if not pims.tiff_stack.libtiff_available(): TiffStack_libtiff = not_available("libtiff") if not pims.tiff_stack.PIL_available(): @@ -97,17 +115,20 @@ def raiser(*args, **kwargs): TiffStack = not_available("tifffile, libtiff, or PIL/Pillow") + try: import pims.bioformats if pims.bioformats.available(): - Bioformats = pims.bioformats.BioformatsReader + Bioformats = register_fmt(pims.bioformats.BioformatsFormat, + 'Bioformats', + 'Reads multidimensional images from a file supported by bioformats.', + 'lsm ipl dm3 seq nd2 ics ids ipw tif tiff' + ' jpg bmp lif lei', 'iIvV') else: raise ImportError() except (ImportError, IOError): - BioformatsRaw = not_available("JPype") Bioformats = not_available("JPype") - try: from pims_nd2 import ND2_Reader as ND2Reader_SDK @@ -161,51 +182,56 @@ def open(sequence, **kwargs): >>> frame_count = len(video) # Number of frames in video >>> frame_shape = video.frame_shape # Pixel dimensions of video """ + # check if it is an ImageSequence files = glob.glob(sequence) if len(files) > 1: # todo: test if ImageSequence can read the image type, # delegate to subclasses as needed return ImageSequence(sequence, **kwargs) - _, ext = os.path.splitext(sequence) - if ext is None or len(ext) < 2: - raise UnknownFormatError( - "Could not detect your file type because it did not have an " - "extension. Try specifying a loader class, e.g. " - "Video({0})".format(sequence)) - ext = ext.lower()[1:] - - # list all readers derived from the pims baseclasses - all_handlers = chain(_recursive_subclasses(FramesSequence), - _recursive_subclasses(FramesSequenceND)) - # keep handlers that support the file ext. use set to avoid duplicates. - eligible_handlers = set(h for h in all_handlers - if ext and ext in map(_drop_dot, h.class_exts())) - if len(eligible_handlers) < 1: - raise UnknownFormatError( - "Could not autodetect how to load a file of type {0}. " - "Try manually " - "specifying a loader class, e.g. Video({1})".format(ext, sequence)) - - def sort_on_priority(handlers): - # This uses optional priority information from subclasses - # > 10 means that it will be used instead of than built-in subclasses - def priority(cls): - try: - return cls.class_priority - except AttributeError: - return 10 - return sorted(handlers, key=priority, reverse=True) - - exceptions = '' - for handler in sort_on_priority(eligible_handlers): - try: - return handler(sequence, **kwargs) - except Exception as e: - message = '{0} errored: {1}'.format(str(handler), str(e)) - warn(message) - exceptions += message + '\n' - raise UnknownFormatError("All handlers returned exceptions:\n" + exceptions) + # the rest of the Reader choosing logic is handled by imageio + return WrapImageIOReader(get_reader(sequence, **kwargs)) + # + # + # _, ext = os.path.splitext(sequence) + # if ext is None or len(ext) < 2: + # raise UnknownFormatError( + # "Could not detect your file type because it did not have an " + # "extension. Try specifying a loader class, e.g. " + # "Video({0})".format(sequence)) + # ext = ext.lower()[1:] + # + # # list all readers derived from the pims baseclasses + # all_handlers = chain(_recursive_subclasses(FramesSequence), + # _recursive_subclasses(FramesSequenceND)) + # # keep handlers that support the file ext. use set to avoid duplicates. + # eligible_handlers = set(h for h in all_handlers + # if ext and ext in map(_drop_dot, h.class_exts())) + # if len(eligible_handlers) < 1: + # raise UnknownFormatError( + # "Could not autodetect how to load a file of type {0}. " + # "Try manually " + # "specifying a loader class, e.g. Video({1})".format(ext, sequence)) + # + # def sort_on_priority(handlers): + # # This uses optional priority information from subclasses + # # > 10 means that it will be used instead of than built-in subclasses + # def priority(cls): + # try: + # return cls.class_priority + # except AttributeError: + # return 10 + # return sorted(handlers, key=priority, reverse=True) + # + # exceptions = '' + # for handler in sort_on_priority(eligible_handlers): + # try: + # return handler(sequence, **kwargs) + # except Exception as e: + # message = '{0} errored: {1}'.format(str(handler), str(e)) + # warn(message) + # exceptions += message + '\n' + # raise UnknownFormatError("All handlers returned exceptions:\n" + exceptions) class UnknownFormatError(Exception): diff --git a/pims/base_frames.py b/pims/base_frames.py index 6962a078..6ebe960e 100644 --- a/pims/base_frames.py +++ b/pims/base_frames.py @@ -8,6 +8,7 @@ from .frame import Frame from abc import ABCMeta, abstractmethod, abstractproperty from warnings import warn +from imageio.core import Format class FramesStream(with_metaclass(ABCMeta, object)): @@ -606,3 +607,122 @@ def __repr__(self): s += "Axis '{0}' size: {1}\n".format(dim, self._sizes[dim]) s += """Pixel Datatype: {dtype}""".format(dtype=self.pixel_type) return s + + +def guess_axes(image): + shape = image.shape + ndim = len(shape) + + if ndim == 2: + return 'yx' + elif ndim == 3 and shape[2] in [3, 4]: + return 'yxc' + elif ndim == 3: + return 'zyx' + elif ndim == 4 and shape[3] in [3, 4]: + return 'zyxc' + else: + raise ValueError("Cannot interpret dimensions for an image of " + "shape {0}".format(shape)) + + +def default_axes(sizes, mode): + if 'i' in mode: # single image + if sizes.get('z', 1) == 1 and sizes.get('t', 1) == 1: + bundle_axes = 'yx' + iter_axes = '' + else: + raise ValueError("This file cannot be opened as single image.") + elif 'I' in mode: # multiple images + if sizes.get('z', 1) == 1 and 't' in sizes: + bundle_axes = 'yx' + iter_axes = 't' + else: + raise ValueError("This file cannot be opened as multiple images.") + elif 'v' in mode: # single volume + if 'z' in sizes and sizes.get('t', 1) == 1: + bundle_axes = 'zyx' + iter_axes = '' + else: + raise ValueError("This file cannot be opened as single volume.") + elif 'V' in mode: # multiple volumes + if 'z' in sizes and 't' in sizes: + bundle_axes = 'zyx' + iter_axes = 't' + else: + raise ValueError("This file cannot be opened as multiple volumes.") + else: + if 'z' in sizes: + bundle_axes = 'zyx' + else: + bundle_axes = 'yx' + if 't' in sizes: + iter_axes = 't' + else: + iter_axes = '' + + if 'c' in sizes: + bundle_axes += 'c' + + return bundle_axes, iter_axes + + +def wrap_get_data(get_data_func, axis='t'): + # takes only kwargs (one index per named axis) + def get_frame(**kwargs): + index = kwargs[axis] + im, md = get_data_func(index) + return Frame(im, frame_no=index, metadata=md) + return get_frame + + +class WrapImageIOReader(FramesSequenceND): + def __init__(self, imageio_reader): + if not isinstance(imageio_reader, Format.Reader): + raise ValueError("Can only wrap ImageIO readers") + + super(WrapImageIOReader, self).__init__() + self.rdr = imageio_reader + self.update_nd() + + def update_nd(self): + self._clear_axes() + self._get_frame_dict = dict() + + try: + info = self.rdr.pims_info + except AttributeError: + info = None + + if info is None: + # guess everything from the first frame + tmp, _ = self.rdr._get_data(0) + self._dtype = tmp.dtype + + axes = guess_axes(tmp) + for name, size in zip(axes, tmp.shape): + self._init_axis(name, size) + self._init_axis('t', self.rdr._get_length()) + self._register_get_frame(wrap_get_data(self.rdr._get_data, 't'), axes) + else: + self._dtype = info['dtype'] + for axis, size in info['sizes'].items(): + self._init_axis(axis, size) + + for name, axes in info['read_methods'].items(): + method = getattr(self.rdr, name) + self._register_get_frame(method, axes) + + self.bundle_axes, self.iter_axes = default_axes(self.sizes, + self.rdr.request.mode) + + def __getattr__(self, key): + return getattr(self.rdr, key) + + @property + def pixel_type(self): + return self._dtype + + @property + def dtype(self): + return self._dtype diff --git a/pims/bioformats.py b/pims/bioformats.py index 85868bf2..20754110 100644 --- a/pims/bioformats.py +++ b/pims/bioformats.py @@ -3,7 +3,7 @@ import numpy as np -from pims.base_frames import FramesSequence, FramesSequenceND +from imageio.core import Format from pims.frame import Frame from warnings import warn import os @@ -197,7 +197,7 @@ def __repr__(self): 'MetadataRetrieve functions: ' + ', '.join(self.fields) -class BioformatsReader(FramesSequenceND): +class BioformatsFormat(Format): """Reads multidimensional images from the frames of a file supported by bioformats into an iterable object that returns images as numpy arrays. The axes inside the numpy array (czyx, zyx, cyx or yx) depend on the @@ -315,264 +315,267 @@ class BioformatsReader(FramesSequenceND): x_um, y_um, z_um: physical location of the image in microns t_s: timestamp of the image in seconds """ - @classmethod - def class_exts(cls): - return {'lsm', 'ipl', 'dm3', 'seq', 'nd2', 'ics', 'ids', - 'ipw', 'tif', 'tiff', 'jpg', 'bmp', 'lif', 'lei'} - - class_priority = 2 + def _can_read(self, request): + """Determine whether `request.filename` can be read using this + Format.Reader, judging from the imageio.core.Request object.""" + if request.mode[1] in (self.modes + '?'): + if request.filename.lower().endswith(self.extensions): + return True + + def _can_write(self, request): + """Determine whether file type `request.filename` can be written using + this Format.Writer, judging from the imageio.core.Request object.""" + return False + + # class_priority = 2 # Unused, ImageIO handles the reader choosing logic propagate_attrs = ['frame_shape', 'pixel_type', 'metadata', 'get_metadata_raw', 'reader_class_name'] - @property - def pixel_type(self): - return self._pixel_type - - def __init__(self, filename, meta=True, java_memory='512m', - read_mode='auto', series=0): - global loci - super(BioformatsReader, self).__init__() - - if read_mode not in ['auto', 'jpype', 'stringbuffer', 'javacasting']: - raise ValueError('Invalid read_mode value.') - - # Make sure that file exists before starting java - if not os.path.isfile(filename): - raise IOError('The file "{}" does not exist.'.format(filename)) - - # Start java VM and initialize logger (globally) - if not jpype.isJVMStarted(): - loci_path = _find_jar() - jpype.startJVM(jpype.getDefaultJVMPath(), '-ea', - '-Djava.class.path=' + loci_path, - '-Xmx' + java_memory) - log4j = jpype.JPackage('org.apache.log4j') - log4j.BasicConfigurator.configure() - log4j_logger = log4j.Logger.getRootLogger() - log4j_logger.setLevel(log4j.Level.ERROR) - - if not jpype.isThreadAttachedToJVM(): - jpype.attachThreadToJVM() - - loci = jpype.JPackage('loci') - - # Initialize reader and metadata - self.filename = str(filename) - self.rdr = loci.formats.ChannelSeparator(loci.formats.ChannelFiller()) - - # patch for issue with ND2 files and the Chunkmap implemented in 5.4.0 - # See https://github.com/openmicroscopy/bioformats/issues/2955 - # circumventing the reserved keyword 'in' - mo = getattr(loci.formats, 'in').DynamicMetadataOptions() - mo.set('nativend2.chunkmap', 'False') # Format Bool as String - self.rdr.setMetadataOptions(mo) - - if meta: - self._metadata = loci.formats.MetadataTools.createOMEXMLMetadata() - self.rdr.setMetadataStore(self._metadata) - self.rdr.setId(self.filename) - if meta: - self.metadata = MetadataRetrieve(self._metadata) - - # Checkout reader dtype and define read mode - isLittleEndian = self.rdr.isLittleEndian() - LE_prefix = ['>', '<'][isLittleEndian] - FormatTools = loci.formats.FormatTools - self._dtype_dict = {FormatTools.INT8: 'i1', - FormatTools.UINT8: 'u1', - FormatTools.INT16: LE_prefix + 'i2', - FormatTools.UINT16: LE_prefix + 'u2', - FormatTools.INT32: LE_prefix + 'i4', - FormatTools.UINT32: LE_prefix + 'u4', - FormatTools.FLOAT: LE_prefix + 'f4', - FormatTools.DOUBLE: LE_prefix + 'f8'} - self._dtype_dict_java = {} - for loci_format in self._dtype_dict.keys(): - self._dtype_dict_java[loci_format] = \ - (FormatTools.getBytesPerPixel(loci_format), - FormatTools.isFloatingPoint(loci_format), - isLittleEndian) - - # Set the correct series and initialize the sizes - self.size_series = self.rdr.getSeriesCount() - if series >= self.size_series or series < 0: - self.rdr.close() - raise IndexError('Series index out of bounds.') - self._series = series - self._change_series() - - # Set read mode. When auto, tryout fast and check the image size. - if read_mode == 'auto': - Jarr = self.rdr.openBytes(0) - if isinstance(Jarr[:], np.ndarray): - read_mode = 'jpype' - else: - warn('Due to an issue with JPype 0.6.0, reading is slower. ' - 'Please consider upgrading JPype to 0.6.1 or later.') - try: - im = self._jbytearr_stringbuffer(Jarr) - im.reshape(self._sizeRGB, self._sizeX, self._sizeY) - except (AttributeError, ValueError): - read_mode = 'javacasting' + class Reader(Format.Reader): + def _open(self, series=0, meta=True, java_memory='512m', + read_mode='auto'): + global loci + self.pims_info = None + + if read_mode not in ['auto', 'jpype', 'stringbuffer', 'javacasting']: + raise ValueError('Invalid read_mode value.') + + filename = str(self.request.get_local_filename()) + + # Make sure that file exists before starting java + if not os.path.isfile(filename): + raise IOError('The file "{}" does not exist.'.format(filename)) + + # Start java VM and initialize logger (globally) + if not jpype.isJVMStarted(): + loci_path = _find_jar() + jpype.startJVM(jpype.getDefaultJVMPath(), '-ea', + '-Djava.class.path=' + loci_path, + '-Xmx' + java_memory) + log4j = jpype.JPackage('org.apache.log4j') + log4j.BasicConfigurator.configure() + log4j_logger = log4j.Logger.getRootLogger() + log4j_logger.setLevel(log4j.Level.ERROR) + + if not jpype.isThreadAttachedToJVM(): + jpype.attachThreadToJVM() + + loci = jpype.JPackage('loci') + + # Initialize reader and metadata + self.filename = str(filename) + self.rdr = loci.formats.ChannelSeparator(loci.formats.ChannelFiller()) + + # patch for issue with ND2 files and the Chunkmap implemented in 5.4.0 + # See https://github.com/openmicroscopy/bioformats/issues/2955 + # circumventing the reserved keyword 'in' + mo = getattr(loci.formats, 'in').DynamicMetadataOptions() + mo.set('nativend2.chunkmap', 'False') # Format Bool as String + self.rdr.setMetadataOptions(mo) + + if meta: + self._metadata = loci.formats.MetadataTools.createOMEXMLMetadata() + self.rdr.setMetadataStore(self._metadata) + self.rdr.setId(self.filename) + if meta: + self.metadata = MetadataRetrieve(self._metadata) + + # Checkout reader dtype and define read mode + isLittleEndian = self.rdr.isLittleEndian() + LE_prefix = ['>', '<'][isLittleEndian] + FormatTools = loci.formats.FormatTools + self._dtype_dict = {FormatTools.INT8: 'i1', + FormatTools.UINT8: 'u1', + FormatTools.INT16: LE_prefix + 'i2', + FormatTools.UINT16: LE_prefix + 'u2', + FormatTools.INT32: LE_prefix + 'i4', + FormatTools.UINT32: LE_prefix + 'u4', + FormatTools.FLOAT: LE_prefix + 'f4', + FormatTools.DOUBLE: LE_prefix + 'f8'} + self._dtype_dict_java = {} + for loci_format in self._dtype_dict.keys(): + self._dtype_dict_java[loci_format] = \ + (FormatTools.getBytesPerPixel(loci_format), + FormatTools.isFloatingPoint(loci_format), + isLittleEndian) + + # Set the correct series and initialize the sizes + self.size_series = self.rdr.getSeriesCount() + self.series = None + try: + self.change_series(series) + except IndexError: + self.rdr.close() + + # Set read mode. When auto, tryout fast and check the image size. + if read_mode == 'auto': + Jarr = self.rdr.openBytes(0) + if isinstance(Jarr[:], np.ndarray): + read_mode = 'jpype' + else: + warn('Due to an issue with JPype 0.6.0, reading is slower. ' + 'Please consider upgrading JPype to 0.6.1 or later.') + try: + im = self._jbytearr_stringbuffer(Jarr) + im.reshape(self._sizeRGB, self._sizeX, self._sizeY) + except (AttributeError, ValueError): + read_mode = 'javacasting' + else: + read_mode = 'stringbuffer' + self.read_mode = read_mode + + # Define the names of the standard per frame metadata. + self.frame_metadata = {} + if meta: + if hasattr(self.metadata, 'PlaneDeltaT'): + self.frame_metadata['t_s'] = 'PlaneDeltaT' + if hasattr(self.metadata, 'PlanePositionX'): + self.frame_metadata['x_um'] = 'PlanePositionX' + if hasattr(self.metadata, 'PlanePositionY'): + self.frame_metadata['y_um'] = 'PlanePositionY' + if hasattr(self.metadata, 'PlanePositionZ'): + self.frame_metadata['z_um'] = 'PlanePositionZ' + + def change_series(self, series): + """Changes series and rereads axes, sizes and metadata.""" + if series >= self.size_series or series < 0: + raise IndexError('Series index out of bounds.') + self.series = series + self.pims_info = dict(read_methods=dict(), sizes=dict(), dtype=None) + + self.rdr.setSeries(series) + sizeX = self.rdr.getSizeX() + sizeY = self.rdr.getSizeY() + sizeT = self.rdr.getSizeT() + sizeZ = self.rdr.getSizeZ() + self.isRGB = self.rdr.isRGB() + self.isInterleaved = self.rdr.isInterleaved() + if self.isRGB: + sizeC = self.rdr.getRGBChannelCount() + if self.isInterleaved: + self._frame_shape_2D = (sizeY, sizeX, sizeC) + self.pims_info['read_methods'] = {'get_frame_2D': 'yxc'} else: - read_mode = 'stringbuffer' - self.read_mode = read_mode - - # Define the names of the standard per frame metadata. - self.frame_metadata = {} - if meta: - if hasattr(self.metadata, 'PlaneDeltaT'): - self.frame_metadata['t_s'] = 'PlaneDeltaT' - if hasattr(self.metadata, 'PlanePositionX'): - self.frame_metadata['x_um'] = 'PlanePositionX' - if hasattr(self.metadata, 'PlanePositionY'): - self.frame_metadata['y_um'] = 'PlanePositionY' - if hasattr(self.metadata, 'PlanePositionZ'): - self.frame_metadata['z_um'] = 'PlanePositionZ' - - def _change_series(self): - """Changes series and rereads axes, sizes and metadata. - """ - series = self._series - self._clear_axes() - self.rdr.setSeries(series) - sizeX = self.rdr.getSizeX() - sizeY = self.rdr.getSizeY() - sizeT = self.rdr.getSizeT() - sizeZ = self.rdr.getSizeZ() - self.isRGB = self.rdr.isRGB() - self.isInterleaved = self.rdr.isInterleaved() - if self.isRGB: - sizeC = self.rdr.getRGBChannelCount() - if self.isInterleaved: - self._frame_shape_2D = (sizeY, sizeX, sizeC) - self._register_get_frame(self.get_frame_2D, 'yxc') + self._frame_shape_2D = (sizeC, sizeY, sizeX) + self.pims_info['read_methods'] = {'get_frame_2D': 'cyx'} else: - self._frame_shape_2D = (sizeC, sizeY, sizeX) - self._register_get_frame(self.get_frame_2D, 'cyx') - else: - sizeC = self.rdr.getSizeC() - self._frame_shape_2D = (sizeY, sizeX) - self._register_get_frame(self.get_frame_2D, 'yx') - - self._init_axis('x', sizeX) - self._init_axis('y', sizeY) - if sizeC > 1: - self._init_axis('c', sizeC) - if sizeT > 1: - self._init_axis('t', sizeT) - if sizeZ > 1: - self._init_axis('z', sizeZ) - - # determine pixel type - pixel_type = self.rdr.getPixelType() - dtype = self._dtype_dict[pixel_type] - java_dtype = self._dtype_dict_java[pixel_type] - - self._jbytearr_stringbuffer = \ - lambda arr: _jbytearr_stringbuffer(arr, dtype) - self._jbytearr_javacasting = \ - lambda arr: _jbytearr_javacasting(arr, dtype, *java_dtype) - self._pixel_type = dtype - - if 'z' in self.axes: - self.bundle_axes = 'zyx' - if 't' in self.axes: - self.iter_axes = 't' - - # get some metadata fields - try: - self.colors = [_jrgba_to_rgb(self.metadata.ChannelColor(series, c)) - for c in range(sizeC)] - except AttributeError: - self.colors = None - try: - self.calibration = self.metadata.PixelsPhysicalSizeX(series) - except AttributeError: + sizeC = self.rdr.getSizeC() + self._frame_shape_2D = (sizeY, sizeX) + self.pims_info['read_methods'] = {'get_frame_2D': 'yx'} + + self.pims_info['sizes'] = {'x': sizeX, 'y': sizeY} + self._len = 1 + if sizeC > 1: + self.pims_info['sizes']['c'] = sizeC + self._len *= sizeC + if sizeT > 1: + self.pims_info['sizes']['t'] = sizeT + self._len *= sizeT + if sizeZ > 1: + self.pims_info['sizes']['z'] = sizeZ + self._len *= sizeZ + + # determine pixel type + pixel_type = self.rdr.getPixelType() + dtype = self._dtype_dict[pixel_type] + java_dtype = self._dtype_dict_java[pixel_type] + + self._jbytearr_stringbuffer = \ + lambda arr: _jbytearr_stringbuffer(arr, dtype) + self._jbytearr_javacasting = \ + lambda arr: _jbytearr_javacasting(arr, dtype, *java_dtype) + + self.pims_info['dtype'] = dtype + + # get some metadata fields + try: + self.colors = [_jrgba_to_rgb(self.metadata.ChannelColor(series, c)) + for c in range(sizeC)] + except AttributeError: + self.colors = None try: - self.calibration = self.metadata.PixelsPhysicalSizeY(series) - except: - self.calibration = None - try: - self.calibrationZ = self.metadata.PixelsPhysicalSizeZ(series) - except AttributeError: - self.calibrationZ = None - - def close(self): - self.rdr.close() - - @property - def series(self): - return self._series - - @series.setter - def series(self, value): - if value >= self.size_series or value < 0: - raise IndexError('Series index out of bounds.') - else: - if value != self._series: - self._series = value - self._change_series() - - def get_frame_2D(self, **coords): - """Actual reader, returns image as 2D numpy array and metadata as - dict. - """ - _coords = {'t': 0, 'c': 0, 'z': 0} - _coords.update(coords) - if self.isRGB: - _coords['c'] = 0 - j = self.rdr.getIndex(int(_coords['z']), int(_coords['c']), - int(_coords['t'])) - if self.read_mode == 'jpype': - im = np.frombuffer(self.rdr.openBytes(j)[:], - dtype=self._pixel_type) - elif self.read_mode == 'stringbuffer': - im = self._jbytearr_stringbuffer(self.rdr.openBytes(j)) - elif self.read_mode == 'javacasting': - im = self._jbytearr_javacasting(self.rdr.openBytes(j)) - - im.shape = self._frame_shape_2D - im = im.astype(self._pixel_type, copy=False) - - metadata = {'frame': j, - 'series': self._series} - if self.colors is not None: - metadata['colors'] = self.colors - if self.calibration is not None: - metadata['mpp'] = self.calibration - if self.calibrationZ is not None: - metadata['mppZ'] = self.calibrationZ - metadata.update(coords) - for key, method in self.frame_metadata.items(): - metadata[key] = getattr(self.metadata, method)(self._series, j) - - return Frame(im, metadata=metadata) - - def get_metadata_raw(self, form='dict'): - hashtable = self.rdr.getGlobalMetadata() - keys = hashtable.keys() - if form == 'dict': - result = {} - while keys.hasMoreElements(): - key = keys.nextElement() - result[key] = _maybe_tostring(hashtable.get(key)) - elif form == 'list': - result = [] - while keys.hasMoreElements(): - key = keys.nextElement() - result.append(key + ': ' + _maybe_tostring(hashtable.get(key))) - elif form == 'string': - result = u'' - while keys.hasMoreElements(): - key = keys.nextElement() - result += key + ': ' + _maybe_tostring(hashtable.get(key)) + '\n' - return result - - @property - def reader_class_name(self): - return self.rdr.getFormat() - - @property - def version(self): - return loci.formats.FormatTools.VERSION + self.calibration = self.metadata.PixelsPhysicalSizeX(series) + except AttributeError: + try: + self.calibration = self.metadata.PixelsPhysicalSizeY(series) + except: + self.calibration = None + try: + self.calibrationZ = self.metadata.PixelsPhysicalSizeZ(series) + except AttributeError: + self.calibrationZ = None + + def _close(self): + self.rdr.close() + + def _get_data(self, j): + if self.read_mode == 'jpype': + im = np.frombuffer(self.rdr.openBytes(j)[:], + dtype=self.pims_info['dtype']) + elif self.read_mode == 'stringbuffer': + im = self._jbytearr_stringbuffer(self.rdr.openBytes(j)) + elif self.read_mode == 'javacasting': + im = self._jbytearr_javacasting(self.rdr.openBytes(j)) + + im.shape = self._frame_shape_2D + return im, self._get_meta_data(j) + + def _get_meta_data(self, j): + if j is None: + return self.get_metadata_raw() + + z, c, t = self.rdr.getZCTCoords(j) + metadata = dict(frame=j, z=z, c=c, t=t, series=self.series) + if self.colors is not None: + metadata['colors'] = self.colors + if self.calibration is not None: + metadata['mpp'] = self.calibration + if self.calibrationZ is not None: + metadata['mppZ'] = self.calibrationZ + for key, method in self.frame_metadata.items(): + metadata[key] = getattr(self.metadata, method)(self.series, j) + return metadata + + def _get_length(self): + return self._len + + def get_frame_2D(self, **coords): + """Actual reader, returns image as 2D numpy array and metadata as + dict. + """ + _coords = {'t': 0, 'c': 0, 'z': 0} + _coords.update(coords) + if self.isRGB: + _coords['c'] = 0 + j = self.rdr.getIndex(int(_coords['z']), int(_coords['c']), + int(_coords['t'])) + im, metadata = self._get_data(j) + return Frame(im, metadata=metadata) + + def get_metadata_raw(self, form='dict'): + hashtable = self.rdr.getGlobalMetadata() + keys = hashtable.keys() + if form == 'dict': + result = {} + while keys.hasMoreElements(): + key = keys.nextElement() + result[key] = _maybe_tostring(hashtable.get(key)) + elif form == 'list': + result = [] + while keys.hasMoreElements(): + key = keys.nextElement() + result.append(key + ': ' + _maybe_tostring(hashtable.get(key))) + elif form == 'string': + result = u'' + while keys.hasMoreElements(): + key = keys.nextElement() + result += key + ': ' + _maybe_tostring(hashtable.get(key)) + '\n' + return result + + @property + def reader_class_name(self): + return self.rdr.getFormat() + + @property + def version(self): + return loci.formats.FormatTools.VERSION diff --git a/pims/tests/data/stuck_metadata_py2.pkl b/pims/tests/data/stuck_metadata_py2.pkl deleted file mode 100644 index b170f9ee..00000000 Binary files a/pims/tests/data/stuck_metadata_py2.pkl and /dev/null differ diff --git a/pims/tests/data/stuck_metadata_py3.pkl b/pims/tests/data/stuck_metadata_py3.pkl deleted file mode 100644 index 5ddf77e2..00000000 Binary files a/pims/tests/data/stuck_metadata_py3.pkl and /dev/null differ diff --git a/pims/tests/test_bioformats.py b/pims/tests/test_bioformats.py index 68f651f0..2bf1061f 100644 --- a/pims/tests/test_bioformats.py +++ b/pims/tests/test_bioformats.py @@ -42,7 +42,7 @@ def test_bool(self): def test_open(self): self.v.close() - self.v = pims.open(self.filename) + self.v = pims.open(self.filename, format='Bioformats') def test_integer_attributes(self): self.check_skip() @@ -109,7 +109,7 @@ def check_skip(self): def test_getting_stack(self): self.check_skip() - assert_equal(self.v[0].shape[-3], self.expected_Z) + assert_equal(self.v[0].shape[0], self.expected_Z) def test_sizeZ(self): self.check_skip() @@ -171,7 +171,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (10, 31, 38) + self.expected_shape = (10, 31, 38, 2) self.expected_len = 3 self.expected_Z = 10 self.expected_C = 2 @@ -195,7 +195,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (240, 320) + self.expected_shape = (240, 320, 3) self.expected_len = 108 self.expected_C = 3 @@ -220,7 +220,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (24, 256, 256) + self.expected_shape = (24, 256, 256, 2) self.expected_len = 7 self.expected_C = 2 self.expected_Z = 24 @@ -267,7 +267,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (21, 300, 400) + self.expected_shape = (21, 300, 400, 2) self.expected_len = 19 self.expected_C = 2 self.expected_Z = 21 @@ -292,7 +292,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (4, 256, 256) + self.expected_shape = (4, 256, 256, 2) self.expected_len = 5 self.expected_C = 2 self.expected_Z = 4 @@ -345,7 +345,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False, 'series': 0} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (25, 512, 512) + self.expected_shape = (25, 512, 512, 4) self.expected_len = 1 self.expected_C = 4 self.expected_Z = 25 @@ -354,7 +354,8 @@ def test_count_series(self): assert_equal(self.v.size_series, 2) def test_switch_series(self): - self.v.series = 1 + self.v.change_series(1) + self.v.update_nd() assert_equal(self.v.sizes['z'], 46) def tearDown(self): @@ -375,7 +376,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False, 'series': 1} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (46, 512, 512) + self.expected_shape = (46, 512, 512, 4) self.expected_len = 1 self.expected_C = 4 self.expected_Z = 46 @@ -384,7 +385,7 @@ def tearDown(self): self.v.close() -class TestBioformatsIPL(_image_single, unittest.TestCase): +class TestBioformatsIPL(_image_single,_image_multichannel, unittest.TestCase): # IPLab format, 650 x 515 pixels, 8 bits per sample, 3 channels # Scanalytics has provided a sample multi-channel image in IPLab format. def check_skip(self): @@ -398,7 +399,8 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (515, 650) + self.expected_shape = (515, 650, 3) + self.expected_C = 3 self.expected_len = 1 def tearDown(self): @@ -491,7 +493,7 @@ def setUp(self): self.klass = pims.Bioformats self.kwargs = {'meta': False} self.v = self.klass(self.filename, **self.kwargs) - self.expected_shape = (29, 672, 512) + self.expected_shape = (29, 672, 512, 3) self.expected_C = 3 self.expected_Z = 29 diff --git a/pims/tests/test_common.py b/pims/tests/test_common.py index 44612dc7..ee111010 100644 --- a/pims/tests/test_common.py +++ b/pims/tests/test_common.py @@ -14,6 +14,8 @@ from numpy.testing import (assert_equal, assert_allclose) from nose.tools import assert_true import pims +import imageio +from datetime import datetime path, _ = os.path.split(os.path.abspath(__file__)) path = os.path.join(path, 'data') @@ -446,7 +448,11 @@ def setUp(self): self.frame1 = np.load(os.path.join(path, 'bulk-water_frame1.npy')) self.klass = pims.ImageIOReader self.kwargs = dict() - self.v = self.klass(self.filename, **self.kwargs) + try: + self.v = self.klass(self.filename, **self.kwargs) + except imageio.core.fetching.NeedDownloadError: + imageio.plugins.ffmpeg.download() + self.v = self.klass(self.filename, **self.kwargs) self.expected_shape = (424, 640, 3) self.expected_len = 480 @@ -476,13 +482,11 @@ def tearDown(self): class _tiff_image_series(_image_series): def test_metadata(self): m = self.v[0].metadata - if sys.version_info.major < 3: - pkl_path = os.path.join(path, 'stuck_metadata_py2.pkl') - else: - pkl_path = os.path.join(path, 'stuck_metadata_py3.pkl') - with open(pkl_path, 'rb') as p: - d = pickle.load(p) - assert_equal(m, d) + expected = {'Software': 'tifffile.py', + 'DateTime': datetime(2015, 1, 18, 15, 33, 49), + 'ImageDescription': 'shape=(5,512,512)'} + for key in expected: + assert_equal(m[key], expected[key]) class TestTiffStack_libtiff(_tiff_image_series, unittest.TestCase): @@ -566,6 +570,7 @@ def setUp(self): _skip_if_no_PIL() def test_open_png(self): + _skip_if_no_imread() self.filenames = ['dummy_png.png'] shape = (10, 11) save_dummy_png(path, self.filenames, shape) @@ -573,6 +578,7 @@ def test_open_png(self): clean_dummy_png(path, self.filenames) def test_open_pngs(self): + _skip_if_no_imread() self.filepath = os.path.join(path, 'image_sequence') self.filenames = ['T76S3F00001.png', 'T76S3F00002.png', 'T76S3F00003.png', 'T76S3F00004.png', diff --git a/pims/tiff_stack.py b/pims/tiff_stack.py index abbded15..afb39a98 100644 --- a/pims/tiff_stack.py +++ b/pims/tiff_stack.py @@ -8,6 +8,7 @@ import itertools import numpy as np from pims.frame import Frame +from imageio.core import Format try: from PIL import Image # should work with PIL or PILLOW @@ -53,7 +54,7 @@ def _tiff_datetime(dt_str): minute=int(dt_str[14:16]), second=int(dt_str[17:19])) -class TiffStack_tifffile(FramesSequence): +class FormatTiffStack_tifffile(Format): """Read TIFF stacks (single files containing many images) into an iterable object that returns images as numpy arrays. @@ -97,73 +98,75 @@ class TiffStack_tifffile(FramesSequence): -------- TiffStack_pil, TiffStack_libtiff, ImageSequence """ - @classmethod - def class_exts(cls): - # TODO extend this set to match reality - return {'tif', 'tiff', 'lsm', - 'stk'} | super(TiffStack_tifffile, cls).class_exts() - - def __init__(self, filename): - self._filename = filename - record = tifffile.TiffFile(filename).series[0] - if hasattr(record, 'pages'): - self._tiff = record.pages - else: - self._tiff = record['pages'] - - tmp = self._tiff[0] - self._dtype = tmp.dtype - self._im_sz = tmp.shape - - def get_frame(self, j): - t = self._tiff[j] - data = t.asarray() - return Frame(data, frame_no=j, metadata=self._read_metadata(t)) - - def _read_metadata(self, tiff): - """Read metadata for current frame and return as dict""" - md = {} - try: - md["ImageDescription"] = ( - tiff.tags["image_description"].value.decode()) - except: - pass - try: - dt = tiff.tags["datetime"].value.decode() - md["DateTime"] = _tiff_datetime(dt) - except: - pass - try: - md["Software"] = tiff.tags["software"].value.decode() - except: + def _can_read(self, request): + """Determine whether `request.filename` can be read using this + Format.Reader, judging from the imageio.core.Request object.""" + if request.mode[1] in (self.modes + '?'): + if request.filename.lower().endswith(self.extensions): + return True + + def _can_write(self, request): + """Determine whether file type `request.filename` can be written using + this Format.Writer, judging from the imageio.core.Request object.""" + return False + + class Reader(Format.Reader): + def _open(self, **kwargs): + # Specify kwargs here. Optionally, the user-specified kwargs + # can also be accessed via the request.kwargs object. + # + # The request object provides two ways to get access to the + # data. Use just one: + # - Use request.get_file() for a file object (preferred) + # - Use request.get_local_filename() for a file on the system + handle = self.request.get_file() + record = tifffile.TiffFile(handle, **kwargs).series[0] + if hasattr(record, 'pages'): + self._tiff = record.pages + else: + self._tiff = record['pages'] + + def _close(self): + # Close the reader. + # Note that the request object will close self._fp pass - try: - md["DocumentName"] = tiff.tags["document_name"].value.decode() - except: - pass - return md - @property - def pixel_type(self): - return self._dtype + def _get_length(self): + # Return the number of images. Can be np.inf + return len(self._tiff) - @property - def frame_shape(self): - return self._im_sz + def _get_data(self, index): + tiff = self._tiff[index] + return tiff.asarray(), self._read_metadata(tiff) - def __len__(self): - return len(self._tiff) + def _get_meta_data(self, index): + # Get the meta data for the given index. If index is None, it + # should return the global meta data. + tiff = self._tiff[index] + return self._read_metadata(tiff) - def __repr__(self): - # May be overwritten by subclasses - return """ -Source: {filename} -Length: {count} frames -Frame Shape: {frame_shape!r} -Pixel Datatype: {dtype}""".format(frame_shape=self.frame_shape, - count=len(self), - filename=self._filename, - dtype=self.pixel_type) + def _read_metadata(self, tiff): + """Read metadata for current frame and return as dict""" + md = {} + try: + md["ImageDescription"] = ( + tiff.tags["image_description"].value.decode()) + except: + pass + try: + dt = tiff.tags["datetime"].value.decode() + md["DateTime"] = _tiff_datetime(dt) + except: + pass + try: + md["Software"] = tiff.tags["software"].value.decode() + except: + pass + try: + md["DocumentName"] = tiff.tags["document_name"].value.decode() + except: + pass + return md class TiffStack_libtiff(FramesSequence): diff --git a/setup.py b/setup.py index 6402ab0d..34006d43 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ cmdclass=versioneer.get_cmdclass(), description="Python Image Sequence", author="PIMS Contributors", - install_requires=['slicerator>=0.9.7', 'six>=1.8', 'numpy>=1.7'], + install_requires=['slicerator>=0.9.7', 'six>=1.8', 'numpy>=1.7', + 'pillow', 'imageio'], author_email="dallan@pha.jhu.edu", url="https://github.com/soft-matter/pims", packages=['pims',