diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py
new file mode 100644
index 00000000..da90abdf
--- /dev/null
+++ b/skimage/external/tifffile/__init__.py
@@ -0,0 +1,4 @@
+try:
+ from tifffile import *
+except ImportError:
+ from ._tifffile import *
diff --git a/skimage/external/tifffile/_tifffile.py b/skimage/external/tifffile/_tifffile.py
new file mode 100644
index 00000000..83ae4f0e
--- /dev/null
+++ b/skimage/external/tifffile/_tifffile.py
@@ -0,0 +1,4839 @@
+
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# tifffile.py
+
+# Copyright (c) 2008-2014, Christoph Gohlke
+# Copyright (c) 2008-2014, The Regents of the University of California
+# Produced at the Laboratory for Fluorescence Dynamics
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the copyright holders nor the names of any
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Read and write image data from and to TIFF files.
+
+Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH,
+SGI, ImageJ, MicroManager, FluoView, SEQ and GEL files.
+Only a subset of the TIFF specification is supported, mainly uncompressed
+and losslessly compressed 2**(0 to 6) bit integer, 16, 32 and 64-bit float,
+grayscale and RGB(A) images, which are commonly used in bio-scientific imaging.
+Specifically, reading JPEG and CCITT compressed image data or EXIF, IPTC, GPS,
+and XMP metadata is not implemented.
+Only primary info records are read for STK, FluoView, MicroManager, and
+NIH image formats.
+
+TIFF, the Tagged Image File Format, is under the control of Adobe Systems.
+BigTIFF allows for files greater than 4 GB. STK, LSM, FluoView, SGI, SEQ, GEL,
+and OME-TIFF, are custom extensions defined by Molecular Devices (Universal
+Imaging Corporation), Carl Zeiss MicroImaging, Olympus, Silicon Graphics
+International, Media Cybernetics, Molecular Dynamics, and the Open Microscopy
+Environment consortium respectively.
+
+For command line usage run ``python tifffile.py --help``
+
+:Author:
+ `Christoph Gohlke `_
+
+:Organization:
+ Laboratory for Fluorescence Dynamics, University of California, Irvine
+
+:Version: 2014.08.24
+
+Requirements
+------------
+* `CPython 2.7 or 3.4 `_
+* `Numpy 1.8.2 `_
+* `Matplotlib 1.4 `_ (optional for plotting)
+* `Tifffile.c 2013.11.05 `_
+ (recommended for faster decoding of PackBits and LZW encoded strings)
+
+Notes
+-----
+The API is not stable yet and might change between revisions.
+
+Tested on little-endian platforms only.
+
+Other Python packages and modules for reading bio-scientific TIFF files:
+
+* `Imread `_
+* `PyLibTiff `_
+* `SimpleITK `_
+* `PyLSM `_
+* `PyMca.TiffIO.py `_ (same as fabio.TiffIO)
+* `BioImageXD.Readers `_
+* `Cellcognition.io `_
+* `CellProfiler.bioformats
+ `_
+
+Acknowledgements
+----------------
+* Egor Zindy, University of Manchester, for cz_lsm_scan_info specifics.
+* Wim Lewis for a bug fix and some read_cz_lsm functions.
+* Hadrien Mary for help on reading MicroManager files.
+
+References
+----------
+(1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
+ http://partners.adobe.com/public/developer/tiff/
+(2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html
+(3) MetaMorph Stack (STK) Image File Format.
+ http://support.meta.moleculardevices.com/docs/t10243.pdf
+(4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
+ Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
+(5) File Format Description - LSM 5xx Release 2.0.
+ http://ibb.gsf.de/homepage/karsten.rodenacker/IDL/Lsmfile.doc
+(6) The OME-TIFF format.
+ http://www.openmicroscopy.org/site/support/file-formats/ome-tiff
+(7) UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
+ http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
+(8) Micro-Manager File Formats.
+ http://www.micro-manager.org/wiki/Micro-Manager_File_Formats
+(9) Tags for TIFF and Related Specifications. Digital Preservation.
+ http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
+
+Examples
+--------
+>>> data = numpy.random.rand(5, 301, 219)
+>>> imsave('temp.tif', data)
+
+>>> image = imread('temp.tif')
+>>> numpy.testing.assert_array_equal(image, data)
+
+>>> with TiffFile('temp.tif') as tif:
+... images = tif.asarray()
+... for page in tif:
+... for tag in page.tags.values():
+... t = tag.name, tag.value
+... image = page.asarray()
+
+"""
+
+from __future__ import division, print_function
+
+import sys
+import os
+import re
+import glob
+import math
+import zlib
+import time
+import json
+import struct
+import warnings
+import tempfile
+import datetime
+import collections
+from fractions import Fraction
+from xml.etree import cElementTree as etree
+
+import numpy
+
+__version__ = '0.3.1'
+__docformat__ = 'restructuredtext en'
+__all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter',
+ 'TiffSequence')
+
+
+def imsave(filename, data, **kwargs):
+ """Write image data to TIFF file.
+
+ Refer to the TiffWriter class and member functions for documentation.
+
+ Parameters
+ ----------
+ filename : str
+ Name of file to write.
+ data : array_like
+ Input image. The last dimensions are assumed to be image depth,
+ height, width, and samples.
+ kwargs : dict
+ Parameters 'byteorder', 'bigtiff', and 'software' are passed to
+ the TiffWriter class.
+ Parameters 'photometric', 'planarconfig', 'resolution',
+ 'description', 'compress', 'volume', and 'extratags' are passed to
+ the TiffWriter.save function.
+
+ Examples
+ --------
+ >>> data = numpy.random.rand(2, 5, 3, 301, 219)
+ >>> description = u'{"shape": %s}' % str(list(data.shape))
+ >>> imsave('temp.tif', data, compress=6,
+ ... extratags=[(270, 's', 0, description, True)])
+
+ """
+ tifargs = {}
+ for key in ('byteorder', 'bigtiff', 'software', 'writeshape'):
+ if key in kwargs:
+ tifargs[key] = kwargs[key]
+ del kwargs[key]
+
+ if 'writeshape' not in kwargs:
+ kwargs['writeshape'] = True
+ if 'bigtiff' not in tifargs and data.size*data.dtype.itemsize > 2000*2**20:
+ tifargs['bigtiff'] = True
+
+ with TiffWriter(filename, **tifargs) as tif:
+ tif.save(data, **kwargs)
+
+
+class TiffWriter(object):
+ """Write image data to TIFF file.
+
+ TiffWriter instances must be closed using the close method, which is
+ automatically called when using the 'with' statement.
+
+ Examples
+ --------
+ >>> data = numpy.random.rand(2, 5, 3, 301, 219)
+ >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
+ ... for i in range(data.shape[0]):
+ ... tif.save(data[i], compress=6)
+
+ """
+ TYPES = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6,
+ 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17}
+ TAGS = {
+ 'new_subfile_type': 254, 'subfile_type': 255,
+ 'image_width': 256, 'image_length': 257, 'bits_per_sample': 258,
+ 'compression': 259, 'photometric': 262, 'fill_order': 266,
+ 'document_name': 269, 'image_description': 270, 'strip_offsets': 273,
+ 'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278,
+ 'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283,
+ 'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296,
+ 'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320,
+ 'tile_width': 322, 'tile_length': 323, 'tile_offsets': 324,
+ 'tile_byte_counts': 325, 'extra_samples': 338, 'sample_format': 339,
+ 'image_depth': 32997, 'tile_depth': 32998}
+
+ def __init__(self, filename, bigtiff=False, byteorder=None,
+ software='tifffile.py'):
+ """Create a new TIFF file for writing.
+
+ Use bigtiff=True when creating files greater than 2 GB.
+
+ Parameters
+ ----------
+ filename : str
+ Name of file to write.
+ bigtiff : bool
+ If True, the BigTIFF format is used.
+ byteorder : {'<', '>'}
+ The endianness of the data in the file.
+ By default this is the system's native byte order.
+ software : str
+ Name of the software used to create the image.
+ Saved with the first page only.
+
+ """
+ if byteorder not in (None, '<', '>'):
+ raise ValueError("invalid byteorder %s" % byteorder)
+ if byteorder is None:
+ byteorder = '<' if sys.byteorder == 'little' else '>'
+
+ self._byteorder = byteorder
+ self._software = software
+
+ self._fh = open(filename, 'wb')
+ self._fh.write({'<': b'II', '>': b'MM'}[byteorder])
+
+ if bigtiff:
+ self._bigtiff = True
+ self._offset_size = 8
+ self._tag_size = 20
+ self._numtag_format = 'Q'
+ self._offset_format = 'Q'
+ self._val_format = '8s'
+ self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0))
+ else:
+ self._bigtiff = False
+ self._offset_size = 4
+ self._tag_size = 12
+ self._numtag_format = 'H'
+ self._offset_format = 'I'
+ self._val_format = '4s'
+ self._fh.write(struct.pack(byteorder+'H', 42))
+
+ # first IFD
+ self._ifd_offset = self._fh.tell()
+ self._fh.write(struct.pack(byteorder+self._offset_format, 0))
+
+ def save(self, data, photometric=None, planarconfig=None, resolution=None,
+ description=None, volume=False, writeshape=False, compress=0,
+ extratags=()):
+ """Write image data to TIFF file.
+
+ Image data are written in one stripe per plane.
+ Dimensions larger than 2 to 4 (depending on photometric mode, planar
+ configuration, and SGI mode) are flattened and saved as separate pages.
+ The 'sample_format' and 'bits_per_sample' TIFF tags are derived from
+ the data type.
+
+ Parameters
+ ----------
+ data : array_like
+ Input image. The last dimensions are assumed to be image depth,
+ height, width, and samples.
+ photometric : {'minisblack', 'miniswhite', 'rgb'}
+ The color space of the image data.
+ By default this setting is inferred from the data shape.
+ planarconfig : {'contig', 'planar'}
+ Specifies if samples are stored contiguous or in separate planes.
+ By default this setting is inferred from the data shape.
+ 'contig': last dimension contains samples.
+ 'planar': third last dimension contains samples.
+ resolution : (float, float) or ((int, int), (int, int))
+ X and Y resolution in dots per inch as float or rational numbers.
+ description : str
+ The subject of the image. Saved with the first page only.
+ compress : int
+ Values from 0 to 9 controlling the level of zlib compression.
+ If 0, data are written uncompressed (default).
+ volume : bool
+ If True, volume data are stored in one tile (if applicable) using
+ the SGI image_depth and tile_depth tags.
+ Image width and depth must be multiple of 16.
+ Few software can read this format, e.g. MeVisLab.
+ writeshape : bool
+ If True, write the data shape to the image_description tag
+ if necessary and no other description is given.
+ extratags: sequence of tuples
+ Additional tags as [(code, dtype, count, value, writeonce)].
+
+ code : int
+ The TIFF tag Id.
+ dtype : str
+ Data type of items in 'value' in Python struct format.
+ One of B, s, H, I, 2I, b, h, i, f, d, Q, or q.
+ count : int
+ Number of data values. Not used for string values.
+ value : sequence
+ 'Count' values compatible with 'dtype'.
+ writeonce : bool
+ If True, the tag is written to the first page only.
+
+ """
+ if photometric not in (None, 'minisblack', 'miniswhite', 'rgb'):
+ raise ValueError("invalid photometric %s" % photometric)
+ if planarconfig not in (None, 'contig', 'planar'):
+ raise ValueError("invalid planarconfig %s" % planarconfig)
+ if not 0 <= compress <= 9:
+ raise ValueError("invalid compression level %s" % compress)
+
+ fh = self._fh
+ byteorder = self._byteorder
+ numtag_format = self._numtag_format
+ val_format = self._val_format
+ offset_format = self._offset_format
+ offset_size = self._offset_size
+ tag_size = self._tag_size
+
+ data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C')
+ data_shape = shape = data.shape
+ data = numpy.atleast_2d(data)
+
+ # normalize shape of data
+ samplesperpixel = 1
+ extrasamples = 0
+ if volume and data.ndim < 3:
+ volume = False
+ if photometric is None:
+ if planarconfig:
+ photometric = 'rgb'
+ elif data.ndim > 2 and shape[-1] in (3, 4):
+ photometric = 'rgb'
+ elif volume and data.ndim > 3 and shape[-4] in (3, 4):
+ photometric = 'rgb'
+ elif data.ndim > 2 and shape[-3] in (3, 4):
+ photometric = 'rgb'
+ else:
+ photometric = 'minisblack'
+ if planarconfig and len(shape) <= (3 if volume else 2):
+ planarconfig = None
+ photometric = 'minisblack'
+ if photometric == 'rgb':
+ if len(shape) < 3:
+ raise ValueError("not a RGB(A) image")
+ if len(shape) < 4:
+ volume = False
+ if planarconfig is None:
+ if shape[-1] in (3, 4):
+ planarconfig = 'contig'
+ elif shape[-4 if volume else -3] in (3, 4):
+ planarconfig = 'planar'
+ elif shape[-1] > shape[-4 if volume else -3]:
+ planarconfig = 'planar'
+ else:
+ planarconfig = 'contig'
+ if planarconfig == 'contig':
+ data = data.reshape((-1, 1) + shape[(-4 if volume else -3):])
+ samplesperpixel = data.shape[-1]
+ else:
+ data = data.reshape(
+ (-1,) + shape[(-4 if volume else -3):] + (1,))
+ samplesperpixel = data.shape[1]
+ if samplesperpixel > 3:
+ extrasamples = samplesperpixel - 3
+ elif planarconfig and len(shape) > (3 if volume else 2):
+ if planarconfig == 'contig':
+ data = data.reshape((-1, 1) + shape[(-4 if volume else -3):])
+ samplesperpixel = data.shape[-1]
+ else:
+ data = data.reshape(
+ (-1,) + shape[(-4 if volume else -3):] + (1,))
+ samplesperpixel = data.shape[1]
+ extrasamples = samplesperpixel - 1
+ else:
+ planarconfig = None
+ # remove trailing 1s
+ while len(shape) > 2 and shape[-1] == 1:
+ shape = shape[:-1]
+ if len(shape) < 3:
+ volume = False
+ if False and (
+ len(shape) > (3 if volume else 2) and shape[-1] < 5 and
+ all(shape[-1] < i
+ for i in shape[(-4 if volume else -3):-1])):
+ # DISABLED: non-standard TIFF, e.g. (220, 320, 2)
+ planarconfig = 'contig'
+ samplesperpixel = shape[-1]
+ data = data.reshape((-1, 1) + shape[(-4 if volume else -3):])
+ else:
+ data = data.reshape(
+ (-1, 1) + shape[(-3 if volume else -2):] + (1,))
+
+ if samplesperpixel == 2:
+ warnings.warn("writing non-standard TIFF (samplesperpixel 2)")
+
+ if volume and (data.shape[-2] % 16 or data.shape[-3] % 16):
+ warnings.warn("volume width or length are not multiple of 16")
+ volume = False
+ data = numpy.swapaxes(data, 1, 2)
+ data = data.reshape(
+ (data.shape[0] * data.shape[1],) + data.shape[2:])
+
+ # data.shape is now normalized 5D or 6D, depending on volume
+ # (pages, planar_samples, (depth,) height, width, contig_samples)
+ assert len(data.shape) in (5, 6)
+ shape = data.shape
+
+ bytestr = bytes if sys.version[0] == '2' else (
+ lambda x: bytes(x, 'utf-8') if isinstance(x, str) else x)
+ tags = [] # list of (code, ifdentry, ifdvalue, writeonce)
+
+ if volume:
+ # use tiles to save volume data
+ tag_byte_counts = TiffWriter.TAGS['tile_byte_counts']
+ tag_offsets = TiffWriter.TAGS['tile_offsets']
+ else:
+ # else use strips
+ tag_byte_counts = TiffWriter.TAGS['strip_byte_counts']
+ tag_offsets = TiffWriter.TAGS['strip_offsets']
+
+ def pack(fmt, *val):
+ return struct.pack(byteorder+fmt, *val)
+
+ def addtag(code, dtype, count, value, writeonce=False):
+ # Compute ifdentry & ifdvalue bytes from code, dtype, count, value.
+ # Append (code, ifdentry, ifdvalue, writeonce) to tags list.
+ code = int(TiffWriter.TAGS.get(code, code))
+ try:
+ tifftype = TiffWriter.TYPES[dtype]
+ except KeyError:
+ raise ValueError("unknown dtype %s" % dtype)
+ rawcount = count
+ if dtype == 's':
+ value = bytestr(value) + b'\0'
+ count = rawcount = len(value)
+ value = (value, )
+ if len(dtype) > 1:
+ count *= int(dtype[:-1])
+ dtype = dtype[-1]
+ ifdentry = [pack('HH', code, tifftype),
+ pack(offset_format, rawcount)]
+ ifdvalue = None
+ if count == 1:
+ if isinstance(value, (tuple, list)):
+ value = value[0]
+ ifdentry.append(pack(val_format, pack(dtype, value)))
+ elif struct.calcsize(dtype) * count <= offset_size:
+ ifdentry.append(pack(val_format,
+ pack(str(count)+dtype, *value)))
+ else:
+ ifdentry.append(pack(offset_format, 0))
+ ifdvalue = pack(str(count)+dtype, *value)
+ tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
+
+ def rational(arg, max_denominator=1000000):
+ # return nominator and denominator from float or two integers
+ try:
+ f = Fraction.from_float(arg)
+ except TypeError:
+ f = Fraction(arg[0], arg[1])
+ f = f.limit_denominator(max_denominator)
+ return f.numerator, f.denominator
+
+ if self._software:
+ addtag('software', 's', 0, self._software, writeonce=True)
+ self._software = None # only save to first page
+ if description:
+ addtag('image_description', 's', 0, description, writeonce=True)
+ elif writeshape and shape[0] > 1 and shape != data_shape:
+ addtag('image_description', 's', 0,
+ "shape=(%s)" % (",".join('%i' % i for i in data_shape)),
+ writeonce=True)
+ addtag('datetime', 's', 0,
+ datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"),
+ writeonce=True)
+ addtag('compression', 'H', 1, 32946 if compress else 1)
+ addtag('orientation', 'H', 1, 1)
+ addtag('image_width', 'I', 1, shape[-2])
+ addtag('image_length', 'I', 1, shape[-3])
+ if volume:
+ addtag('image_depth', 'I', 1, shape[-4])
+ addtag('tile_depth', 'I', 1, shape[-4])
+ addtag('tile_width', 'I', 1, shape[-2])
+ addtag('tile_length', 'I', 1, shape[-3])
+ addtag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2)
+ addtag('sample_format', 'H', 1,
+ {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind])
+ addtag('photometric', 'H', 1,
+ {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric])
+ addtag('samples_per_pixel', 'H', 1, samplesperpixel)
+ if planarconfig and samplesperpixel > 1:
+ addtag('planar_configuration', 'H', 1, 1
+ if planarconfig == 'contig' else 2)
+ addtag('bits_per_sample', 'H', samplesperpixel,
+ (data.dtype.itemsize * 8, ) * samplesperpixel)
+ else:
+ addtag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8)
+ if extrasamples:
+ if photometric == 'rgb' and extrasamples == 1:
+ addtag('extra_samples', 'H', 1, 1) # associated alpha channel
+ else:
+ addtag('extra_samples', 'H', extrasamples, (0,) * extrasamples)
+ if resolution:
+ addtag('x_resolution', '2I', 1, rational(resolution[0]))
+ addtag('y_resolution', '2I', 1, rational(resolution[1]))
+ addtag('resolution_unit', 'H', 1, 2)
+ addtag('rows_per_strip', 'I', 1,
+ shape[-3] * (shape[-4] if volume else 1))
+
+ # use one strip or tile per plane
+ strip_byte_counts = (data[0, 0].size * data.dtype.itemsize,) * shape[1]
+ addtag(tag_byte_counts, offset_format, shape[1], strip_byte_counts)
+ addtag(tag_offsets, offset_format, shape[1], (0, ) * shape[1])
+
+ # add extra tags from users
+ for t in extratags:
+ addtag(*t)
+ # the entries in an IFD must be sorted in ascending order by tag code
+ tags = sorted(tags, key=lambda x: x[0])
+
+ if not self._bigtiff and (fh.tell() + data.size*data.dtype.itemsize
+ > 2**31-1):
+ raise ValueError("data too large for non-bigtiff file")
+
+ for pageindex in range(shape[0]):
+ # update pointer at ifd_offset
+ pos = fh.tell()
+ fh.seek(self._ifd_offset)
+ fh.write(pack(offset_format, pos))
+ fh.seek(pos)
+
+ # write ifdentries
+ fh.write(pack(numtag_format, len(tags)))
+ tag_offset = fh.tell()
+ fh.write(b''.join(t[1] for t in tags))
+ self._ifd_offset = fh.tell()
+ fh.write(pack(offset_format, 0)) # offset to next IFD
+
+ # write tag values and patch offsets in ifdentries, if necessary
+ for tagindex, tag in enumerate(tags):
+ if tag[2]:
+ pos = fh.tell()
+ fh.seek(tag_offset + tagindex*tag_size + offset_size + 4)
+ fh.write(pack(offset_format, pos))
+ fh.seek(pos)
+ if tag[0] == tag_offsets:
+ strip_offsets_offset = pos
+ elif tag[0] == tag_byte_counts:
+ strip_byte_counts_offset = pos
+ fh.write(tag[2])
+
+ # write image data
+ data_offset = fh.tell()
+ if compress:
+ strip_byte_counts = []
+ for plane in data[pageindex]:
+ plane = zlib.compress(plane, compress)
+ strip_byte_counts.append(len(plane))
+ fh.write(plane)
+ else:
+ # if this fails try update Python/numpy
+ data[pageindex].tofile(fh)
+ fh.flush()
+
+ # update strip and tile offsets and byte_counts if necessary
+ pos = fh.tell()
+ for tagindex, tag in enumerate(tags):
+ if tag[0] == tag_offsets: # strip or tile offsets
+ if tag[2]:
+ fh.seek(strip_offsets_offset)
+ strip_offset = data_offset
+ for size in strip_byte_counts:
+ fh.write(pack(offset_format, strip_offset))
+ strip_offset += size
+ else:
+ fh.seek(tag_offset + tagindex*tag_size +
+ offset_size + 4)
+ fh.write(pack(offset_format, data_offset))
+ elif tag[0] == tag_byte_counts: # strip or tile byte_counts
+ if compress:
+ if tag[2]:
+ fh.seek(strip_byte_counts_offset)
+ for size in strip_byte_counts:
+ fh.write(pack(offset_format, size))
+ else:
+ fh.seek(tag_offset + tagindex*tag_size +
+ offset_size + 4)
+ fh.write(pack(offset_format, strip_byte_counts[0]))
+ break
+ fh.seek(pos)
+ fh.flush()
+ # remove tags that should be written only once
+ if pageindex == 0:
+ tags = [t for t in tags if not t[-1]]
+
+ def close(self):
+ self._fh.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+
+def imread(files, **kwargs):
+ """Return image data from TIFF file(s) as numpy array.
+
+ The first image series is returned if no arguments are provided.
+
+ Parameters
+ ----------
+ files : str or list
+ File name, glob pattern, or list of file names.
+ key : int, slice, or sequence of page indices
+ Defines which pages to return as array.
+ series : int
+ Defines which series of pages in file to return as array.
+ multifile : bool
+ If True (default), OME-TIFF data may include pages from multiple files.
+ pattern : str
+ Regular expression pattern that matches axes names and indices in
+ file names.
+ kwargs : dict
+ Additional parameters passed to the TiffFile or TiffSequence asarray
+ function.
+
+ Examples
+ --------
+ >>> im = imread('test.tif', key=0)
+ >>> im.shape
+ (256, 256, 4)
+ >>> ims = imread(['test.tif', 'test.tif'])
+ >>> ims.shape
+ (2, 256, 256, 4)
+
+ """
+ kwargs_file = {}
+ if 'multifile' in kwargs:
+ kwargs_file['multifile'] = kwargs['multifile']
+ del kwargs['multifile']
+ else:
+ kwargs_file['multifile'] = True
+ kwargs_seq = {}
+ if 'pattern' in kwargs:
+ kwargs_seq['pattern'] = kwargs['pattern']
+ del kwargs['pattern']
+
+ if isinstance(files, basestring) and any(i in files for i in '?*'):
+ files = glob.glob(files)
+ if not files:
+ raise ValueError('no files found')
+ if len(files) == 1:
+ files = files[0]
+
+ if isinstance(files, basestring):
+ with TiffFile(files, **kwargs_file) as tif:
+ return tif.asarray(**kwargs)
+ else:
+ with TiffSequence(files, **kwargs_seq) as imseq:
+ return imseq.asarray(**kwargs)
+
+
+class lazyattr(object):
+ """Lazy object attribute whose value is computed on first access."""
+ __slots__ = ('func', )
+
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+ value = self.func(instance)
+ if value is NotImplemented:
+ return getattr(super(owner, instance), self.func.__name__)
+ setattr(instance, self.func.__name__, value)
+ return value
+
+
+class TiffFile(object):
+ """Read image and metadata from TIFF, STK, LSM, and FluoView files.
+
+ TiffFile instances must be closed using the close method, which is
+ automatically called when using the 'with' statement.
+
+ Attributes
+ ----------
+ pages : list
+ All TIFF pages in file.
+ series : list of Records(shape, dtype, axes, TiffPages)
+ TIFF pages with compatible shapes and types.
+ micromanager_metadata: dict
+ Extra MicroManager non-TIFF metadata in the file, if exists.
+
+ All attributes are read-only.
+
+ Examples
+ --------
+ >>> with TiffFile('test.tif') as tif:
+ ... data = tif.asarray()
+ ... data.shape
+ (256, 256, 4)
+
+ """
+ def __init__(self, arg, name=None, offset=None, size=None,
+ multifile=True, multifile_close=True):
+ """Initialize instance from file.
+
+ Parameters
+ ----------
+ arg : str or open file
+ Name of file or open file object.
+ The file objects are closed in TiffFile.close().
+ name : str
+ Optional name of file in case 'arg' is a file handle.
+ offset : int
+ Optional start position of embedded file. By default this is
+ the current file position.
+ size : int
+ Optional size of embedded file. By default this is the number
+ of bytes from the 'offset' to the end of the file.
+ multifile : bool
+ If True (default), series may include pages from multiple files.
+ Currently applies to OME-TIFF only.
+ multifile_close : bool
+ If True (default), keep the handles of other files in multifile
+ series closed. This is inefficient when few files refer to
+ many pages. If False, the C runtime may run out of resources.
+
+ """
+ self._fh = FileHandle(arg, name=name, offset=offset, size=size)
+ self.offset_size = None
+ self.pages = []
+ self._multifile = bool(multifile)
+ self._multifile_close = bool(multifile_close)
+ self._files = {self._fh.name: self} # cache of TiffFiles
+ try:
+ self._fromfile()
+ except Exception:
+ self._fh.close()
+ raise
+
+ @property
+ def filehandle(self):
+ """Return file handle."""
+ return self._fh
+
+ @property
+ def filename(self):
+ """Return name of file handle."""
+ return self._fh.name
+
+ def close(self):
+ """Close open file handle(s)."""
+ for tif in self._files.values():
+ tif._fh.close()
+ self._files = {}
+
+ def _fromfile(self):
+ """Read TIFF header and all page records from file."""
+ self._fh.seek(0)
+ try:
+ self.byteorder = {b'II': '<', b'MM': '>'}[self._fh.read(2)]
+ except KeyError:
+ raise ValueError("not a valid TIFF file")
+ version = struct.unpack(self.byteorder+'H', self._fh.read(2))[0]
+ if version == 43: # BigTiff
+ self.offset_size, zero = struct.unpack(self.byteorder+'HH',
+ self._fh.read(4))
+ if zero or self.offset_size != 8:
+ raise ValueError("not a valid BigTIFF file")
+ elif version == 42:
+ self.offset_size = 4
+ else:
+ raise ValueError("not a TIFF file")
+ self.pages = []
+ while True:
+ try:
+ page = TiffPage(self)
+ self.pages.append(page)
+ except StopIteration:
+ break
+ if not self.pages:
+ raise ValueError("empty TIFF file")
+
+ if self.is_micromanager:
+ # MicroManager files contain metadata not stored in TIFF tags.
+ self.micromanager_metadata = read_micromanager_metadata(self._fh)
+
+ if self.is_lsm:
+ self._fix_lsm_strip_offsets()
+ self._fix_lsm_strip_byte_counts()
+
+ def _fix_lsm_strip_offsets(self):
+ """Unwrap strip offsets for LSM files greater than 4 GB."""
+ for series in self.series:
+ wrap = 0
+ previous_offset = 0
+ for page in series.pages:
+ strip_offsets = []
+ for current_offset in page.strip_offsets:
+ if current_offset < previous_offset:
+ wrap += 2**32
+ strip_offsets.append(current_offset + wrap)
+ previous_offset = current_offset
+ page.strip_offsets = tuple(strip_offsets)
+
+ def _fix_lsm_strip_byte_counts(self):
+ """Set strip_byte_counts to size of compressed data.
+
+ The strip_byte_counts tag in LSM files contains the number of bytes
+ for the uncompressed data.
+
+ """
+ if not self.pages:
+ return
+ strips = {}
+ for page in self.pages:
+ assert len(page.strip_offsets) == len(page.strip_byte_counts)
+ for offset, bytecount in zip(page.strip_offsets,
+ page.strip_byte_counts):
+ strips[offset] = bytecount
+ offsets = sorted(strips.keys())
+ offsets.append(min(offsets[-1] + strips[offsets[-1]], self._fh.size))
+ for i, offset in enumerate(offsets[:-1]):
+ strips[offset] = min(strips[offset], offsets[i+1] - offset)
+ for page in self.pages:
+ if page.compression:
+ page.strip_byte_counts = tuple(
+ strips[offset] for offset in page.strip_offsets)
+
+ @lazyattr
+ def series(self):
+ """Return series of TiffPage with compatible shape and properties."""
+ if not self.pages:
+ return []
+
+ series = []
+ page0 = self.pages[0]
+
+ if self.is_ome:
+ series = self._omeseries()
+ elif self.is_fluoview:
+ dims = {b'X': 'X', b'Y': 'Y', b'Z': 'Z', b'T': 'T',
+ b'WAVELENGTH': 'C', b'TIME': 'T', b'XY': 'R',
+ b'EVENT': 'V', b'EXPOSURE': 'L'}
+ mmhd = list(reversed(page0.mm_header.dimensions))
+ series = [Record(
+ axes=''.join(dims.get(i[0].strip().upper(), 'Q')
+ for i in mmhd if i[1] > 1),
+ shape=tuple(int(i[1]) for i in mmhd if i[1] > 1),
+ pages=self.pages, dtype=numpy.dtype(page0.dtype))]
+ elif self.is_lsm:
+ lsmi = page0.cz_lsm_info
+ axes = CZ_SCAN_TYPES[lsmi.scan_type]
+ if page0.is_rgb:
+ axes = axes.replace('C', '').replace('XY', 'XYC')
+ axes = axes[::-1]
+ shape = tuple(getattr(lsmi, CZ_DIMENSIONS[i]) for i in axes)
+ pages = [p for p in self.pages if not p.is_reduced]
+ series = [Record(axes=axes, shape=shape, pages=pages,
+ dtype=numpy.dtype(pages[0].dtype))]
+ if len(pages) != len(self.pages): # reduced RGB pages
+ pages = [p for p in self.pages if p.is_reduced]
+ cp = 1
+ i = 0
+ while cp < len(pages) and i < len(shape)-2:
+ cp *= shape[i]
+ i += 1
+ shape = shape[:i] + pages[0].shape
+ axes = axes[:i] + 'CYX'
+ series.append(Record(axes=axes, shape=shape, pages=pages,
+ dtype=numpy.dtype(pages[0].dtype)))
+ elif self.is_imagej:
+ shape = []
+ axes = []
+ ij = page0.imagej_tags
+ if 'frames' in ij:
+ shape.append(ij['frames'])
+ axes.append('T')
+ if 'slices' in ij:
+ shape.append(ij['slices'])
+ axes.append('Z')
+ if 'channels' in ij and not self.is_rgb:
+ shape.append(ij['channels'])
+ axes.append('C')
+ remain = len(self.pages) // (product(shape) if shape else 1)
+ if remain > 1:
+ shape.append(remain)
+ axes.append('I')
+ shape.extend(page0.shape)
+ axes.extend(page0.axes)
+ axes = ''.join(axes)
+ series = [Record(pages=self.pages, shape=tuple(shape), axes=axes,
+ dtype=numpy.dtype(page0.dtype))]
+ elif self.is_nih:
+ if len(self.pages) == 1:
+ shape = page0.shape
+ axes = page0.axes
+ else:
+ shape = (len(self.pages),) + page0.shape
+ axes = 'I' + page0.axes
+ series = [Record(pages=self.pages, shape=shape, axes=axes,
+ dtype=numpy.dtype(page0.dtype))]
+ elif page0.is_shaped:
+ # TODO: shaped files can contain multiple series
+ shape = page0.tags['image_description'].value[7:-1]
+ shape = tuple(int(i) for i in shape.split(b','))
+ series = [Record(pages=self.pages, shape=shape,
+ axes='Q' * len(shape),
+ dtype=numpy.dtype(page0.dtype))]
+
+ # generic detection of series
+ if not series:
+ shapes = []
+ pages = {}
+ for page in self.pages:
+ if not page.shape:
+ continue
+ shape = page.shape + (page.axes,
+ page.compression in TIFF_DECOMPESSORS)
+ if shape not in pages:
+ shapes.append(shape)
+ pages[shape] = [page]
+ else:
+ pages[shape].append(page)
+ series = [Record(pages=pages[s],
+ axes=(('I' + s[-2])
+ if len(pages[s]) > 1 else s[-2]),
+ dtype=numpy.dtype(pages[s][0].dtype),
+ shape=((len(pages[s]), ) + s[:-2]
+ if len(pages[s]) > 1 else s[:-2]))
+ for s in shapes]
+
+ # remove empty series, e.g. in MD Gel files
+ series = [s for s in series if sum(s.shape) > 0]
+
+ return series
+
+ def asarray(self, key=None, series=None, memmap=False):
+ """Return image data from multiple TIFF pages as numpy array.
+
+ By default the first image series is returned.
+
+ Parameters
+ ----------
+ key : int, slice, or sequence of page indices
+ Defines which pages to return as array.
+ series : int
+ Defines which series of pages to return as array.
+ memmap : bool
+ If True, return an array stored in a binary file on disk
+ if possible.
+
+ """
+ if key is None and series is None:
+ series = 0
+ if series is not None:
+ pages = self.series[series].pages
+ else:
+ pages = self.pages
+
+ if key is None:
+ pass
+ elif isinstance(key, int):
+ pages = [pages[key]]
+ elif isinstance(key, slice):
+ pages = pages[key]
+ elif isinstance(key, collections.Iterable):
+ pages = [pages[k] for k in key]
+ else:
+ raise TypeError("key must be an int, slice, or sequence")
+
+ if not len(pages):
+ raise ValueError("no pages selected")
+
+ if self.is_nih:
+ if pages[0].is_palette:
+ result = stack_pages(pages, colormapped=False, squeeze=False)
+ result = numpy.take(pages[0].color_map, result, axis=1)
+ result = numpy.swapaxes(result, 0, 1)
+ else:
+ result = stack_pages(pages, memmap=memmap,
+ colormapped=False, squeeze=False)
+ elif len(pages) == 1:
+ return pages[0].asarray(memmap=memmap)
+ elif self.is_ome:
+ assert not self.is_palette, "color mapping disabled for ome-tiff"
+ if any(p is None for p in pages):
+ # zero out missing pages
+ firstpage = next(p for p in pages if p)
+ nopage = numpy.zeros_like(
+ firstpage.asarray(memmap=False))
+ s = self.series[series]
+ if memmap:
+ with tempfile.NamedTemporaryFile() as fh:
+ result = numpy.memmap(fh, dtype=s.dtype, shape=s.shape)
+ result = result.reshape(-1)
+ else:
+ result = numpy.empty(s.shape, s.dtype).reshape(-1)
+ index = 0
+
+ class KeepOpen:
+ # keep Tiff files open between consecutive pages
+ def __init__(self, parent, close):
+ self.master = parent
+ self.parent = parent
+ self._close = close
+
+ def open(self, page):
+ if self._close and page and page.parent != self.parent:
+ if self.parent != self.master:
+ self.parent.filehandle.close()
+ self.parent = page.parent
+ self.parent.filehandle.open()
+
+ def close(self):
+ if self._close and self.parent != self.master:
+ self.parent.filehandle.close()
+
+ keep = KeepOpen(self, self._multifile_close)
+ for page in pages:
+ keep.open(page)
+ if page:
+ a = page.asarray(memmap=False, colormapped=False,
+ reopen=False)
+ else:
+ a = nopage
+ try:
+ result[index:index + a.size] = a.reshape(-1)
+ except ValueError as e:
+ warnings.warn("ome-tiff: %s" % e)
+ break
+ index += a.size
+ keep.close()
+ else:
+ result = stack_pages(pages, memmap=memmap)
+
+ if key is None:
+ try:
+ result.shape = self.series[series].shape
+ except ValueError:
+ try:
+ warnings.warn("failed to reshape %s to %s" % (
+ result.shape, self.series[series].shape))
+ # try series of expected shapes
+ result.shape = (-1,) + self.series[series].shape
+ except ValueError:
+ # revert to generic shape
+ result.shape = (-1,) + pages[0].shape
+ else:
+ result.shape = (-1,) + pages[0].shape
+ return result
+
+ def _omeseries(self):
+ """Return image series in OME-TIFF file(s)."""
+ root = etree.fromstring(self.pages[0].tags['image_description'].value)
+ uuid = root.attrib.get('UUID', None)
+ self._files = {uuid: self}
+ dirname = self._fh.dirname
+ modulo = {}
+ result = []
+ for element in root:
+ if element.tag.endswith('BinaryOnly'):
+ warnings.warn("ome-xml: not an ome-tiff master file")
+ break
+ if element.tag.endswith('StructuredAnnotations'):
+ for annot in element:
+ if not annot.attrib.get('Namespace',
+ '').endswith('modulo'):
+ continue
+ for value in annot:
+ for modul in value:
+ for along in modul:
+ if not along.tag[:-1].endswith('Along'):
+ continue
+ axis = along.tag[-1]
+ newaxis = along.attrib.get('Type', 'other')
+ newaxis = AXES_LABELS[newaxis]
+ if 'Start' in along.attrib:
+ labels = range(
+ int(along.attrib['Start']),
+ int(along.attrib['End']) + 1,
+ int(along.attrib.get('Step', 1)))
+ else:
+ labels = [label.text for label in along
+ if label.tag.endswith('Label')]
+ modulo[axis] = (newaxis, labels)
+ if not element.tag.endswith('Image'):
+ continue
+ for pixels in element:
+ if not pixels.tag.endswith('Pixels'):
+ continue
+ atr = pixels.attrib
+ dtype = atr.get('Type', None)
+ axes = ''.join(reversed(atr['DimensionOrder']))
+ shape = list(int(atr['Size'+ax]) for ax in axes)
+ size = product(shape[:-2])
+ ifds = [None] * size
+ for data in pixels:
+ if not data.tag.endswith('TiffData'):
+ continue
+ atr = data.attrib
+ ifd = int(atr.get('IFD', 0))
+ num = int(atr.get('NumPlanes', 1 if 'IFD' in atr else 0))
+ num = int(atr.get('PlaneCount', num))
+ idx = [int(atr.get('First'+ax, 0)) for ax in axes[:-2]]
+ try:
+ idx = numpy.ravel_multi_index(idx, shape[:-2])
+ except ValueError:
+ # ImageJ produces invalid ome-xml when cropping
+ warnings.warn("ome-xml: invalid TiffData index")
+ continue
+ for uuid in data:
+ if not uuid.tag.endswith('UUID'):
+ continue
+ if uuid.text not in self._files:
+ if not self._multifile:
+ # abort reading multifile OME series
+ # and fall back to generic series
+ return []
+ fname = uuid.attrib['FileName']
+ try:
+ tif = TiffFile(os.path.join(dirname, fname))
+ except (IOError, ValueError):
+ tif.close()
+ warnings.warn(
+ "ome-xml: failed to read '%s'" % fname)
+ break
+ self._files[uuid.text] = tif
+ if self._multifile_close:
+ tif.close()
+ pages = self._files[uuid.text].pages
+ try:
+ for i in range(num if num else len(pages)):
+ ifds[idx + i] = pages[ifd + i]
+ except IndexError:
+ warnings.warn("ome-xml: index out of range")
+ # only process first uuid
+ break
+ else:
+ pages = self.pages
+ try:
+ for i in range(num if num else len(pages)):
+ ifds[idx + i] = pages[ifd + i]
+ except IndexError:
+ warnings.warn("ome-xml: index out of range")
+ if all(i is None for i in ifds):
+ # skip images without data
+ continue
+ dtype = next(i for i in ifds if i).dtype
+ result.append(Record(axes=axes, shape=shape, pages=ifds,
+ dtype=numpy.dtype(dtype)))
+
+ for record in result:
+ for axis, (newaxis, labels) in modulo.items():
+ i = record.axes.index(axis)
+ size = len(labels)
+ if record.shape[i] == size:
+ record.axes = record.axes.replace(axis, newaxis, 1)
+ else:
+ record.shape[i] //= size
+ record.shape.insert(i+1, size)
+ record.axes = record.axes.replace(axis, axis+newaxis, 1)
+ record.shape = tuple(record.shape)
+
+ # squeeze dimensions
+ for record in result:
+ record.shape, record.axes = squeeze_axes(record.shape, record.axes)
+
+ return result
+
+ def __len__(self):
+ """Return number of image pages in file."""
+ return len(self.pages)
+
+ def __getitem__(self, key):
+ """Return specified page."""
+ return self.pages[key]
+
+ def __iter__(self):
+ """Return iterator over pages."""
+ return iter(self.pages)
+
+ def __str__(self):
+ """Return string containing information about file."""
+ result = [
+ self._fh.name.capitalize(),
+ format_size(self._fh.size),
+ {'<': 'little endian', '>': 'big endian'}[self.byteorder]]
+ if self.is_bigtiff:
+ result.append("bigtiff")
+ if len(self.pages) > 1:
+ result.append("%i pages" % len(self.pages))
+ if len(self.series) > 1:
+ result.append("%i series" % len(self.series))
+ if len(self._files) > 1:
+ result.append("%i files" % (len(self._files)))
+ return ", ".join(result)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ @lazyattr
+ def fstat(self):
+ try:
+ return os.fstat(self._fh.fileno())
+ except Exception: # io.UnsupportedOperation
+ return None
+
+ @lazyattr
+ def is_bigtiff(self):
+ return self.offset_size != 4
+
+ @lazyattr
+ def is_rgb(self):
+ return all(p.is_rgb for p in self.pages)
+
+ @lazyattr
+ def is_palette(self):
+ return all(p.is_palette for p in self.pages)
+
+ @lazyattr
+ def is_mdgel(self):
+ return any(p.is_mdgel for p in self.pages)
+
+ @lazyattr
+ def is_mediacy(self):
+ return any(p.is_mediacy for p in self.pages)
+
+ @lazyattr
+ def is_stk(self):
+ return all(p.is_stk for p in self.pages)
+
+ @lazyattr
+ def is_lsm(self):
+ return self.pages[0].is_lsm
+
+ @lazyattr
+ def is_imagej(self):
+ return self.pages[0].is_imagej
+
+ @lazyattr
+ def is_micromanager(self):
+ return self.pages[0].is_micromanager
+
+ @lazyattr
+ def is_nih(self):
+ return self.pages[0].is_nih
+
+ @lazyattr
+ def is_fluoview(self):
+ return self.pages[0].is_fluoview
+
+ @lazyattr
+ def is_ome(self):
+ return self.pages[0].is_ome
+
+
+class TiffPage(object):
+ """A TIFF image file directory (IFD).
+
+ Attributes
+ ----------
+ index : int
+ Index of page in file.
+ dtype : str {TIFF_SAMPLE_DTYPES}
+ Data type of image, colormapped if applicable.
+ shape : tuple
+ Dimensions of the image array in TIFF page,
+ colormapped and with one alpha channel if applicable.
+ axes : str
+ Axes label codes:
+ 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane,
+ 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda,
+ 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime,
+ 'L' exposure, 'V' event, 'Q' unknown, '_' missing
+ tags : TiffTags
+ Dictionary of tags in page.
+ Tag values are also directly accessible as attributes.
+ color_map : numpy array
+ Color look up table, if exists.
+ cz_lsm_scan_info: Record(dict)
+ LSM scan info attributes, if exists.
+ imagej_tags: Record(dict)
+ Consolidated ImageJ description and metadata tags, if exists.
+ uic_tags: Record(dict)
+ Consolidated MetaMorph STK/UIC tags, if exists.
+
+ All attributes are read-only.
+
+ Notes
+ -----
+ The internal, normalized '_shape' attribute is 6 dimensional:
+
+ 0. number planes (stk)
+ 1. planar samples_per_pixel
+ 2. image_depth Z (sgi)
+ 3. image_length Y
+ 4. image_width X
+ 5. contig samples_per_pixel
+
+ """
+ def __init__(self, parent):
+ """Initialize instance from file."""
+ self.parent = parent
+ self.index = len(parent.pages)
+ self.shape = self._shape = ()
+ self.dtype = self._dtype = None
+ self.axes = ""
+ self.tags = TiffTags()
+
+ self._fromfile()
+ self._process_tags()
+
+ def _fromfile(self):
+ """Read TIFF IFD structure and its tags from file.
+
+ File cursor must be at storage position of IFD offset and is left at
+ offset to next IFD.
+
+ Raises StopIteration if offset (first bytes read) is 0.
+
+ """
+ fh = self.parent.filehandle
+ byteorder = self.parent.byteorder
+ offset_size = self.parent.offset_size
+
+ fmt = {4: 'I', 8: 'Q'}[offset_size]
+ offset = struct.unpack(byteorder + fmt, fh.read(offset_size))[0]
+ if not offset:
+ raise StopIteration()
+
+ # read standard tags
+ tags = self.tags
+ fh.seek(offset)
+ fmt, size = {4: ('H', 2), 8: ('Q', 8)}[offset_size]
+ try:
+ numtags = struct.unpack(byteorder + fmt, fh.read(size))[0]
+ except Exception:
+ warnings.warn("corrupted page list")
+ raise StopIteration()
+
+ tagcode = 0
+ for _ in range(numtags):
+ try:
+ tag = TiffTag(self.parent)
+ # print(tag)
+ except TiffTag.Error as e:
+ warnings.warn(str(e))
+ continue
+ if tagcode > tag.code:
+ # expected for early LSM and tifffile versions
+ warnings.warn("tags are not ordered by code")
+ tagcode = tag.code
+ if tag.name not in tags:
+ tags[tag.name] = tag
+ else:
+ # some files contain multiple IFD with same code
+ # e.g. MicroManager files contain two image_description
+ i = 1
+ while True:
+ name = "%s_%i" % (tag.name, i)
+ if name not in tags:
+ tags[name] = tag
+ break
+
+ pos = fh.tell()
+
+ if self.is_lsm or (self.index and self.parent.is_lsm):
+ # correct non standard LSM bitspersample tags
+ self.tags['bits_per_sample']._correct_lsm_bitspersample(self)
+
+ if self.is_lsm:
+ # read LSM info subrecords
+ for name, reader in CZ_LSM_INFO_READERS.items():
+ try:
+ offset = self.cz_lsm_info['offset_'+name]
+ except KeyError:
+ continue
+ if offset < 8:
+ # older LSM revision
+ continue
+ fh.seek(offset)
+ try:
+ setattr(self, 'cz_lsm_'+name, reader(fh))
+ except ValueError:
+ pass
+
+ elif self.is_stk and 'uic1tag' in tags and not tags['uic1tag'].value:
+ # read uic1tag now that plane count is known
+ uic1tag = tags['uic1tag']
+ fh.seek(uic1tag.value_offset)
+ tags['uic1tag'].value = Record(
+ read_uic1tag(fh, byteorder, uic1tag.dtype, uic1tag.count,
+ tags['uic2tag'].count))
+ fh.seek(pos)
+
+ def _process_tags(self):
+ """Validate standard tags and initialize attributes.
+
+ Raise ValueError if tag values are not supported.
+
+ """
+ tags = self.tags
+ for code, (name, default, dtype, count, validate) in TIFF_TAGS.items():
+ if not (name in tags or default is None):
+ tags[name] = TiffTag(code, dtype=dtype, count=count,
+ value=default, name=name)
+ if name in tags and validate:
+ try:
+ if tags[name].count == 1:
+ setattr(self, name, validate[tags[name].value])
+ else:
+ setattr(self, name, tuple(
+ validate[value] for value in tags[name].value))
+ except KeyError:
+ raise ValueError("%s.value (%s) not supported" %
+ (name, tags[name].value))
+
+ tag = tags['bits_per_sample']
+ if tag.count == 1:
+ self.bits_per_sample = tag.value
+ else:
+ # LSM might list more items than samples_per_pixel
+ value = tag.value[:self.samples_per_pixel]
+ if any((v-value[0] for v in value)):
+ self.bits_per_sample = value
+ else:
+ self.bits_per_sample = value[0]
+
+ tag = tags['sample_format']
+ if tag.count == 1:
+ self.sample_format = TIFF_SAMPLE_FORMATS[tag.value]
+ else:
+ value = tag.value[:self.samples_per_pixel]
+ if any((v-value[0] for v in value)):
+ self.sample_format = [TIFF_SAMPLE_FORMATS[v] for v in value]
+ else:
+ self.sample_format = TIFF_SAMPLE_FORMATS[value[0]]
+
+ if 'photometric' not in tags:
+ self.photometric = None
+
+ if 'image_depth' not in tags:
+ self.image_depth = 1
+
+ if 'image_length' in tags:
+ self.strips_per_image = int(math.floor(
+ float(self.image_length + self.rows_per_strip - 1) /
+ self.rows_per_strip))
+ else:
+ self.strips_per_image = 0
+
+ key = (self.sample_format, self.bits_per_sample)
+ self.dtype = self._dtype = TIFF_SAMPLE_DTYPES.get(key, None)
+
+ if 'image_length' not in self.tags or 'image_width' not in self.tags:
+ # some GEL file pages are missing image data
+ self.image_length = 0
+ self.image_width = 0
+ self.image_depth = 0
+ self.strip_offsets = 0
+ self._shape = ()
+ self.shape = ()
+ self.axes = ''
+
+ if self.is_palette:
+ self.dtype = self.tags['color_map'].dtype[1]
+ self.color_map = numpy.array(self.color_map, self.dtype)
+ dmax = self.color_map.max()
+ if dmax < 256:
+ self.dtype = numpy.uint8
+ self.color_map = self.color_map.astype(self.dtype)
+ #else:
+ # self.dtype = numpy.uint8
+ # self.color_map >>= 8
+ # self.color_map = self.color_map.astype(self.dtype)
+ self.color_map.shape = (3, -1)
+
+ # determine shape of data
+ image_length = self.image_length
+ image_width = self.image_width
+ image_depth = self.image_depth
+ samples_per_pixel = self.samples_per_pixel
+
+ if self.is_stk:
+ assert self.image_depth == 1
+ planes = self.tags['uic2tag'].count
+ if self.is_contig:
+ self._shape = (planes, 1, 1, image_length, image_width,
+ samples_per_pixel)
+ if samples_per_pixel == 1:
+ self.shape = (planes, image_length, image_width)
+ self.axes = 'YX'
+ else:
+ self.shape = (planes, image_length, image_width,
+ samples_per_pixel)
+ self.axes = 'YXS'
+ else:
+ self._shape = (planes, samples_per_pixel, 1, image_length,
+ image_width, 1)
+ if samples_per_pixel == 1:
+ self.shape = (planes, image_length, image_width)
+ self.axes = 'YX'
+ else:
+ self.shape = (planes, samples_per_pixel, image_length,
+ image_width)
+ self.axes = 'SYX'
+ # detect type of series
+ if planes == 1:
+ self.shape = self.shape[1:]
+ elif numpy.all(self.uic2tag.z_distance != 0):
+ self.axes = 'Z' + self.axes
+ elif numpy.all(numpy.diff(self.uic2tag.time_created) != 0):
+ self.axes = 'T' + self.axes
+ else:
+ self.axes = 'I' + self.axes
+ # DISABLED
+ if self.is_palette:
+ assert False, "color mapping disabled for stk"
+ if self.color_map.shape[1] >= 2**self.bits_per_sample:
+ if image_depth == 1:
+ self.shape = (3, planes, image_length, image_width)
+ else:
+ self.shape = (3, planes, image_depth, image_length,
+ image_width)
+ self.axes = 'C' + self.axes
+ else:
+ warnings.warn("palette cannot be applied")
+ self.is_palette = False
+ elif self.is_palette:
+ samples = 1
+ if 'extra_samples' in self.tags:
+ samples += len(self.extra_samples)
+ if self.is_contig:
+ self._shape = (1, 1, image_depth, image_length, image_width,
+ samples)
+ else:
+ self._shape = (1, samples, image_depth, image_length,
+ image_width, 1)
+ if self.color_map.shape[1] >= 2**self.bits_per_sample:
+ if image_depth == 1:
+ self.shape = (3, image_length, image_width)
+ self.axes = 'CYX'
+ else:
+ self.shape = (3, image_depth, image_length, image_width)
+ self.axes = 'CZYX'
+ else:
+ warnings.warn("palette cannot be applied")
+ self.is_palette = False
+ if image_depth == 1:
+ self.shape = (image_length, image_width)
+ self.axes = 'YX'
+ else:
+ self.shape = (image_depth, image_length, image_width)
+ self.axes = 'ZYX'
+ elif self.is_rgb or samples_per_pixel > 1:
+ if self.is_contig:
+ self._shape = (1, 1, image_depth, image_length, image_width,
+ samples_per_pixel)
+ if image_depth == 1:
+ self.shape = (image_length, image_width, samples_per_pixel)
+ self.axes = 'YXS'
+ else:
+ self.shape = (image_depth, image_length, image_width,
+ samples_per_pixel)
+ self.axes = 'ZYXS'
+ else:
+ self._shape = (1, samples_per_pixel, image_depth,
+ image_length, image_width, 1)
+ if image_depth == 1:
+ self.shape = (samples_per_pixel, image_length, image_width)
+ self.axes = 'SYX'
+ else:
+ self.shape = (samples_per_pixel, image_depth,
+ image_length, image_width)
+ self.axes = 'SZYX'
+ if False and self.is_rgb and 'extra_samples' in self.tags:
+ # DISABLED: only use RGB and first alpha channel if exists
+ extra_samples = self.extra_samples
+ if self.tags['extra_samples'].count == 1:
+ extra_samples = (extra_samples, )
+ for exs in extra_samples:
+ if exs in ('unassalpha', 'assocalpha', 'unspecified'):
+ if self.is_contig:
+ self.shape = self.shape[:-1] + (4,)
+ else:
+ self.shape = (4,) + self.shape[1:]
+ break
+ else:
+ self._shape = (1, 1, image_depth, image_length, image_width, 1)
+ if image_depth == 1:
+ self.shape = (image_length, image_width)
+ self.axes = 'YX'
+ else:
+ self.shape = (image_depth, image_length, image_width)
+ self.axes = 'ZYX'
+ if not self.compression and 'strip_byte_counts' not in tags:
+ self.strip_byte_counts = (
+ product(self.shape) * (self.bits_per_sample // 8), )
+
+ assert len(self.shape) == len(self.axes)
+
+ def asarray(self, squeeze=True, colormapped=True, rgbonly=False,
+ scale_mdgel=False, memmap=False, reopen=True):
+ """Read image data from file and return as numpy array.
+
+ Raise ValueError if format is unsupported.
+ If any of 'squeeze', 'colormapped', or 'rgbonly' are not the default,
+ the shape of the returned array might be different from the page shape.
+
+ Parameters
+ ----------
+ squeeze : bool
+ If True, all length-1 dimensions (except X and Y) are
+ squeezed out from result.
+ colormapped : bool
+ If True, color mapping is applied for palette-indexed images.
+ rgbonly : bool
+ If True, return RGB(A) image without additional extra samples.
+ memmap : bool
+ If True, use numpy.memmap to read arrays from file if possible.
+ For use on 64 bit systems and files with few huge contiguous data.
+ reopen : bool
+ If True and the parent file handle is closed, the file is
+ temporarily re-opened (and closed if no exception occurs).
+ scale_mdgel : bool
+ If True, MD Gel data will be scaled according to the private
+ metadata in the second TIFF page. The dtype will be float32.
+
+ """
+ if not self._shape:
+ return
+
+ if self.dtype is None:
+ raise ValueError("data type not supported: %s%i" % (
+ self.sample_format, self.bits_per_sample))
+ if self.compression not in TIFF_DECOMPESSORS:
+ raise ValueError("cannot decompress %s" % self.compression)
+ tag = self.tags['sample_format']
+ if tag.count != 1 and any((i-tag.value[0] for i in tag.value)):
+ raise ValueError("sample formats don't match %s" % str(tag.value))
+
+ fh = self.parent.filehandle
+ closed = fh.closed
+ if closed:
+ if reopen:
+ fh.open()
+ else:
+ raise IOError("file handle is closed")
+
+ dtype = self._dtype
+ shape = self._shape
+ image_width = self.image_width
+ image_length = self.image_length
+ image_depth = self.image_depth
+ typecode = self.parent.byteorder + dtype
+ bits_per_sample = self.bits_per_sample
+
+ if self.is_tiled:
+ if 'tile_offsets' in self.tags:
+ byte_counts = self.tile_byte_counts
+ offsets = self.tile_offsets
+ else:
+ byte_counts = self.strip_byte_counts
+ offsets = self.strip_offsets
+ tile_width = self.tile_width
+ tile_length = self.tile_length
+ tile_depth = self.tile_depth if 'tile_depth' in self.tags else 1
+ tw = (image_width + tile_width - 1) // tile_width
+ tl = (image_length + tile_length - 1) // tile_length
+ td = (image_depth + tile_depth - 1) // tile_depth
+ shape = (shape[0], shape[1],
+ td*tile_depth, tl*tile_length, tw*tile_width, shape[-1])
+ tile_shape = (tile_depth, tile_length, tile_width, shape[-1])
+ runlen = tile_width
+ else:
+ byte_counts = self.strip_byte_counts
+ offsets = self.strip_offsets
+ runlen = image_width
+
+ if any(o < 2 for o in offsets):
+ raise ValueError("corrupted page")
+
+ if memmap and self._is_memmappable(rgbonly, colormapped):
+ result = fh.memmap_array(typecode, shape, offset=offsets[0])
+ elif self.is_contiguous:
+ fh.seek(offsets[0])
+ result = fh.read_array(typecode, product(shape))
+ result = result.astype('=' + dtype)
+ else:
+ if self.is_contig:
+ runlen *= self.samples_per_pixel
+ if bits_per_sample in (8, 16, 32, 64, 128):
+ if (bits_per_sample * runlen) % 8:
+ raise ValueError("data and sample size mismatch")
+
+ def unpack(x):
+ try:
+ return numpy.fromstring(x, typecode)
+ except ValueError as e:
+ # strips may be missing EOI
+ warnings.warn("unpack: %s" % e)
+ xlen = ((len(x) // (bits_per_sample // 8))
+ * (bits_per_sample // 8))
+ return numpy.fromstring(x[:xlen], typecode)
+
+ elif isinstance(bits_per_sample, tuple):
+ def unpack(x):
+ return unpackrgb(x, typecode, bits_per_sample)
+ else:
+ def unpack(x):
+ return unpackints(x, typecode, bits_per_sample, runlen)
+
+ decompress = TIFF_DECOMPESSORS[self.compression]
+ if self.compression == 'jpeg':
+ table = self.jpeg_tables if 'jpeg_tables' in self.tags else b''
+ decompress = lambda x: decodejpg(x, table, self.photometric)
+
+ if self.is_tiled:
+ result = numpy.empty(shape, dtype)
+ tw, tl, td, pl = 0, 0, 0, 0
+ for offset, bytecount in zip(offsets, byte_counts):
+ fh.seek(offset)
+ tile = unpack(decompress(fh.read(bytecount)))
+ tile.shape = tile_shape
+ if self.predictor == 'horizontal':
+ numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile)
+ result[0, pl, td:td+tile_depth,
+ tl:tl+tile_length, tw:tw+tile_width, :] = tile
+ del tile
+ tw += tile_width
+ if tw >= shape[4]:
+ tw, tl = 0, tl + tile_length
+ if tl >= shape[3]:
+ tl, td = 0, td + tile_depth
+ if td >= shape[2]:
+ td, pl = 0, pl + 1
+ result = result[...,
+ :image_depth, :image_length, :image_width, :]
+ else:
+ strip_size = (self.rows_per_strip * self.image_width *
+ self.samples_per_pixel)
+ result = numpy.empty(shape, dtype).reshape(-1)
+ index = 0
+ for offset, bytecount in zip(offsets, byte_counts):
+ fh.seek(offset)
+ strip = fh.read(bytecount)
+ strip = decompress(strip)
+ strip = unpack(strip)
+ size = min(result.size, strip.size, strip_size,
+ result.size - index)
+ result[index:index+size] = strip[:size]
+ del strip
+ index += size
+
+ result.shape = self._shape
+
+ if self.predictor == 'horizontal' and not (self.is_tiled and not
+ self.is_contiguous):
+ # work around bug in LSM510 software
+ if not (self.parent.is_lsm and not self.compression):
+ numpy.cumsum(result, axis=-2, dtype=dtype, out=result)
+
+ if colormapped and self.is_palette:
+ if self.color_map.shape[1] >= 2**bits_per_sample:
+ # FluoView and LSM might fail here
+ result = numpy.take(self.color_map,
+ result[:, 0, :, :, :, 0], axis=1)
+ elif rgbonly and self.is_rgb and 'extra_samples' in self.tags:
+ # return only RGB and first alpha channel if exists
+ extra_samples = self.extra_samples
+ if self.tags['extra_samples'].count == 1:
+ extra_samples = (extra_samples, )
+ for i, exs in enumerate(extra_samples):
+ if exs in ('unassalpha', 'assocalpha', 'unspecified'):
+ if self.is_contig:
+ result = result[..., [0, 1, 2, 3+i]]
+ else:
+ result = result[:, [0, 1, 2, 3+i]]
+ break
+ else:
+ if self.is_contig:
+ result = result[..., :3]
+ else:
+ result = result[:, :3]
+
+ if squeeze:
+ try:
+ result.shape = self.shape
+ except ValueError:
+ warnings.warn("failed to reshape from %s to %s" % (
+ str(result.shape), str(self.shape)))
+
+ if scale_mdgel and self.parent.is_mdgel:
+ # MD Gel stores private metadata in the second page
+ tags = self.parent.pages[1]
+ if tags.md_file_tag in (2, 128):
+ scale = tags.md_scale_pixel
+ scale = scale[0] / scale[1] # rational
+ result = result.astype('float32')
+ if tags.md_file_tag == 2:
+ result **= 2 # squary root data format
+ result *= scale
+
+ if closed:
+ # TODO: file remains open if an exception occurred above
+ fh.close()
+ return result
+
+ def _is_memmappable(self, rgbonly, colormapped):
+ """Return if image data in file can be memory mapped."""
+ if not self.parent.filehandle.is_file or not self.is_contiguous:
+ return False
+ return not (self.predictor or
+ (rgbonly and 'extra_samples' in self.tags) or
+ (colormapped and self.is_palette) or
+ ({'big': '>', 'little': '<'}[sys.byteorder] !=
+ self.parent.byteorder))
+
+ @lazyattr
+ def is_contiguous(self):
+ """Return offset and size of contiguous data, else None.
+
+ Excludes prediction and colormapping.
+
+ """
+ if self.compression or self.bits_per_sample not in (8, 16, 32, 64):
+ return
+ if self.is_tiled:
+ if (self.image_width != self.tile_width or
+ self.image_length % self.tile_length or
+ self.tile_width % 16 or self.tile_length % 16):
+ return
+ if ('image_depth' in self.tags and 'tile_depth' in self.tags and
+ (self.image_length != self.tile_length or
+ self.image_depth % self.tile_depth)):
+ return
+ offsets = self.tile_offsets
+ byte_counts = self.tile_byte_counts
+ else:
+ offsets = self.strip_offsets
+ byte_counts = self.strip_byte_counts
+ if len(offsets) == 1:
+ return offsets[0], byte_counts[0]
+ if self.is_stk or all(offsets[i] + byte_counts[i] == offsets[i+1]
+ or byte_counts[i+1] == 0 # no data/ignore offset
+ for i in range(len(offsets)-1)):
+ return offsets[0], sum(byte_counts)
+
+ def __str__(self):
+ """Return string containing information about page."""
+ s = ', '.join(s for s in (
+ ' x '.join(str(i) for i in self.shape),
+ str(numpy.dtype(self.dtype)),
+ '%s bit' % str(self.bits_per_sample),
+ self.photometric if 'photometric' in self.tags else '',
+ self.compression if self.compression else 'raw',
+ '|'.join(t[3:] for t in (
+ 'is_stk', 'is_lsm', 'is_nih', 'is_ome', 'is_imagej',
+ 'is_micromanager', 'is_fluoview', 'is_mdgel', 'is_mediacy',
+ 'is_sgi', 'is_reduced', 'is_tiled',
+ 'is_contiguous') if getattr(self, t))) if s)
+ return "Page %i: %s" % (self.index, s)
+
+ def __getattr__(self, name):
+ """Return tag value."""
+ if name in self.tags:
+ value = self.tags[name].value
+ setattr(self, name, value)
+ return value
+ raise AttributeError(name)
+
+ @lazyattr
+ def uic_tags(self):
+ """Consolidate UIC tags."""
+ if not self.is_stk:
+ raise AttributeError("uic_tags")
+ tags = self.tags
+ result = Record()
+ result.number_planes = tags['uic2tag'].count
+ if 'image_description' in tags:
+ result.plane_descriptions = self.image_description.split(b'\x00')
+ if 'uic1tag' in tags:
+ result.update(tags['uic1tag'].value)
+ if 'uic3tag' in tags:
+ result.update(tags['uic3tag'].value) # wavelengths
+ if 'uic4tag' in tags:
+ result.update(tags['uic4tag'].value) # override uic1 tags
+ uic2tag = tags['uic2tag'].value
+ result.z_distance = uic2tag.z_distance
+ result.time_created = uic2tag.time_created
+ result.time_modified = uic2tag.time_modified
+ try:
+ result.datetime_created = [
+ julian_datetime(*dt) for dt in
+ zip(uic2tag.date_created, uic2tag.time_created)]
+ result.datetime_modified = [
+ julian_datetime(*dt) for dt in
+ zip(uic2tag.date_modified, uic2tag.time_modified)]
+ except ValueError as e:
+ warnings.warn("uic_tags: %s" % e)
+ return result
+
+ @lazyattr
+ def imagej_tags(self):
+ """Consolidate ImageJ metadata."""
+ if not self.is_imagej:
+ raise AttributeError("imagej_tags")
+ tags = self.tags
+ if 'image_description_1' in tags:
+ # MicroManager
+ result = imagej_description(tags['image_description_1'].value)
+ else:
+ result = imagej_description(tags['image_description'].value)
+ if 'imagej_metadata' in tags:
+ try:
+ result.update(imagej_metadata(
+ tags['imagej_metadata'].value,
+ tags['imagej_byte_counts'].value,
+ self.parent.byteorder))
+ except Exception as e:
+ warnings.warn(str(e))
+ return Record(result)
+
+ @lazyattr
+ def is_rgb(self):
+ """True if page contains a RGB image."""
+ return ('photometric' in self.tags and
+ self.tags['photometric'].value == 2)
+
+ @lazyattr
+ def is_contig(self):
+ """True if page contains a contiguous image."""
+ return ('planar_configuration' in self.tags and
+ self.tags['planar_configuration'].value == 1)
+
+ @lazyattr
+ def is_palette(self):
+ """True if page contains a palette-colored image and not OME or STK."""
+ try:
+ # turn off color mapping for OME-TIFF and STK
+ if self.is_stk or self.is_ome or self.parent.is_ome:
+ return False
+ except IndexError:
+ pass # OME-XML not found in first page
+ return ('photometric' in self.tags and
+ self.tags['photometric'].value == 3)
+
+ @lazyattr
+ def is_tiled(self):
+ """True if page contains tiled image."""
+ return 'tile_width' in self.tags
+
+ @lazyattr
+ def is_reduced(self):
+ """True if page is a reduced image of another image."""
+ return bool(self.tags['new_subfile_type'].value & 1)
+
+ @lazyattr
+ def is_mdgel(self):
+ """True if page contains md_file_tag tag."""
+ return 'md_file_tag' in self.tags
+
+ @lazyattr
+ def is_mediacy(self):
+ """True if page contains Media Cybernetics Id tag."""
+ return ('mc_id' in self.tags and
+ self.tags['mc_id'].value.startswith(b'MC TIFF'))
+
+ @lazyattr
+ def is_stk(self):
+ """True if page contains UIC2Tag tag."""
+ return 'uic2tag' in self.tags
+
+ @lazyattr
+ def is_lsm(self):
+ """True if page contains LSM CZ_LSM_INFO tag."""
+ return 'cz_lsm_info' in self.tags
+
+ @lazyattr
+ def is_fluoview(self):
+ """True if page contains FluoView MM_STAMP tag."""
+ return 'mm_stamp' in self.tags
+
+ @lazyattr
+ def is_nih(self):
+ """True if page contains NIH image header."""
+ return 'nih_image_header' in self.tags
+
+ @lazyattr
+ def is_sgi(self):
+ """True if page contains SGI image and tile depth tags."""
+ return 'image_depth' in self.tags and 'tile_depth' in self.tags
+
+ @lazyattr
+ def is_ome(self):
+ """True if page contains OME-XML in image_description tag."""
+ return ('image_description' in self.tags and self.tags[
+ 'image_description'].value.startswith(b' parent.offset_size or code in CUSTOM_TAGS:
+ pos = fh.tell()
+ tof = {4: 'I', 8: 'Q'}[parent.offset_size]
+ self.value_offset = offset = struct.unpack(byteorder+tof, value)[0]
+ if offset < 0 or offset > parent.filehandle.size:
+ raise TiffTag.Error("corrupt file - invalid tag value offset")
+ elif offset < 4:
+ raise TiffTag.Error("corrupt value offset for tag %i" % code)
+ fh.seek(offset)
+ if code in CUSTOM_TAGS:
+ readfunc = CUSTOM_TAGS[code][1]
+ value = readfunc(fh, byteorder, dtype, count)
+ if isinstance(value, dict): # numpy.core.records.record
+ value = Record(value)
+ elif code in TIFF_TAGS or dtype[-1] == 's':
+ value = struct.unpack(fmt, fh.read(size))
+ else:
+ value = read_numpy(fh, byteorder, dtype, count)
+ fh.seek(pos)
+ else:
+ value = struct.unpack(fmt, value[:size])
+
+ if code not in CUSTOM_TAGS and code not in (273, 279, 324, 325):
+ # scalar value if not strip/tile offsets/byte_counts
+ if len(value) == 1:
+ value = value[0]
+
+ if (dtype.endswith('s') and isinstance(value, bytes)
+ and self._type != 7):
+ # TIFF ASCII fields can contain multiple strings,
+ # each terminated with a NUL
+ value = stripascii(value)
+
+ self.code = code
+ self.name = name
+ self.dtype = dtype
+ self.count = count
+ self.value = value
+
+ def _correct_lsm_bitspersample(self, parent):
+ """Correct LSM bitspersample tag.
+
+ Old LSM writers may use a separate region for two 16-bit values,
+ although they fit into the tag value element of the tag.
+
+ """
+ if self.code == 258 and self.count == 2:
+ # TODO: test this. Need example file.
+ warnings.warn("correcting LSM bitspersample tag")
+ fh = parent.filehandle
+ tof = {4: '')
+
+ def __str__(self):
+ """Return string containing information about tag."""
+ return ' '.join(str(getattr(self, s)) for s in self.__slots__)
+
+
+class TiffSequence(object):
+ """Sequence of image files.
+
+ The data shape and dtype of all files must match.
+
+ Properties
+ ----------
+ files : list
+ List of file names.
+ shape : tuple
+ Shape of image sequence.
+ axes : str
+ Labels of axes in shape.
+
+ Examples
+ --------
+ >>> tifs = TiffSequence("test.oif.files/*.tif")
+ >>> tifs.shape, tifs.axes
+ ((2, 100), 'CT')
+ >>> data = tifs.asarray()
+ >>> data.shape
+ (2, 100, 256, 256)
+
+ """
+ _patterns = {
+ 'axes': r"""
+ # matches Olympus OIF and Leica TIFF series
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
+ """}
+
+ class ParseError(Exception):
+ pass
+
+ def __init__(self, files, imread=TiffFile, pattern='axes',
+ *args, **kwargs):
+ """Initialize instance from multiple files.
+
+ Parameters
+ ----------
+ files : str, or sequence of str
+ Glob pattern or sequence of file names.
+ imread : function or class
+ Image read function or class with asarray function returning numpy
+ array from single file.
+ pattern : str
+ Regular expression pattern that matches axes names and sequence
+ indices in file names.
+ By default this matches Olympus OIF and Leica TIFF series.
+
+ """
+ if isinstance(files, basestring):
+ files = natural_sorted(glob.glob(files))
+ files = list(files)
+ if not files:
+ raise ValueError("no files found")
+ #if not os.path.isfile(files[0]):
+ # raise ValueError("file not found")
+ self.files = files
+
+ if hasattr(imread, 'asarray'):
+ # redefine imread
+ _imread = imread
+
+ def imread(fname, *args, **kwargs):
+ with _imread(fname) as im:
+ return im.asarray(*args, **kwargs)
+
+ self.imread = imread
+
+ self.pattern = self._patterns.get(pattern, pattern)
+ try:
+ self._parse()
+ if not self.axes:
+ self.axes = 'I'
+ except self.ParseError:
+ self.axes = 'I'
+ self.shape = (len(files),)
+ self._start_index = (0,)
+ self._indices = tuple((i,) for i in range(len(files)))
+
+ def __str__(self):
+ """Return string with information about image sequence."""
+ return "\n".join([
+ self.files[0],
+ '* files: %i' % len(self.files),
+ '* axes: %s' % self.axes,
+ '* shape: %s' % str(self.shape)])
+
+ def __len__(self):
+ return len(self.files)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def close(self):
+ pass
+
+ def asarray(self, memmap=False, *args, **kwargs):
+ """Read image data from all files and return as single numpy array.
+
+ If memmap is True, return an array stored in a binary file on disk.
+ The args and kwargs parameters are passed to the imread function.
+
+ Raise IndexError or ValueError if image shapes don't match.
+
+ """
+ im = self.imread(self.files[0], *args, **kwargs)
+ shape = self.shape + im.shape
+ if memmap:
+ with tempfile.NamedTemporaryFile() as fh:
+ result = numpy.memmap(fh, dtype=im.dtype, shape=shape)
+ else:
+ result = numpy.zeros(shape, dtype=im.dtype)
+ result = result.reshape(-1, *im.shape)
+ for index, fname in zip(self._indices, self.files):
+ index = [i-j for i, j in zip(index, self._start_index)]
+ index = numpy.ravel_multi_index(index, self.shape)
+ im = self.imread(fname, *args, **kwargs)
+ result[index] = im
+ result.shape = shape
+ return result
+
+ def _parse(self):
+ """Get axes and shape from file names."""
+ if not self.pattern:
+ raise self.ParseError("invalid pattern")
+ pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE)
+ matches = pattern.findall(self.files[0])
+ if not matches:
+ raise self.ParseError("pattern doesn't match file names")
+ matches = matches[-1]
+ if len(matches) % 2:
+ raise self.ParseError("pattern doesn't match axis name and index")
+ axes = ''.join(m for m in matches[::2] if m)
+ if not axes:
+ raise self.ParseError("pattern doesn't match file names")
+
+ indices = []
+ for fname in self.files:
+ matches = pattern.findall(fname)[-1]
+ if axes != ''.join(m for m in matches[::2] if m):
+ raise ValueError("axes don't match within the image sequence")
+ indices.append([int(m) for m in matches[1::2] if m])
+ shape = tuple(numpy.max(indices, axis=0))
+ start_index = tuple(numpy.min(indices, axis=0))
+ shape = tuple(i-j+1 for i, j in zip(shape, start_index))
+ if product(shape) != len(self.files):
+ warnings.warn("files are missing. Missing data are zeroed")
+
+ self.axes = axes.upper()
+ self.shape = shape
+ self._indices = indices
+ self._start_index = start_index
+
+
+class Record(dict):
+ """Dictionary with attribute access.
+
+ Can also be initialized with numpy.core.records.record.
+
+ """
+ __slots__ = ()
+
+ def __init__(self, arg=None, **kwargs):
+ if kwargs:
+ arg = kwargs
+ elif arg is None:
+ arg = {}
+ try:
+ dict.__init__(self, arg)
+ except (TypeError, ValueError):
+ for i, name in enumerate(arg.dtype.names):
+ v = arg[i]
+ self[name] = v if v.dtype.char != 'S' else stripnull(v)
+
+ def __getattr__(self, name):
+ return self[name]
+
+ def __setattr__(self, name, value):
+ self.__setitem__(name, value)
+
+ def __str__(self):
+ """Pretty print Record."""
+ s = []
+ lists = []
+ for k in sorted(self):
+ try:
+ if k.startswith('_'): # does not work with byte
+ continue
+ except AttributeError:
+ pass
+ v = self[k]
+ if isinstance(v, (list, tuple)) and len(v):
+ if isinstance(v[0], Record):
+ lists.append((k, v))
+ continue
+ elif isinstance(v[0], TiffPage):
+ v = [i.index for i in v if i]
+ s.append(
+ ("* %s: %s" % (k, str(v))).split("\n", 1)[0]
+ [:PRINT_LINE_LEN].rstrip())
+ for k, v in lists:
+ l = []
+ for i, w in enumerate(v):
+ l.append("* %s[%i]\n %s" % (k, i,
+ str(w).replace("\n", "\n ")))
+ s.append('\n'.join(l))
+ return '\n'.join(s)
+
+
+class TiffTags(Record):
+ """Dictionary of TiffTag with attribute access."""
+
+ def __str__(self):
+ """Return string with information about all tags."""
+ s = []
+ for tag in sorted(self.values(), key=lambda x: x.code):
+ typecode = "%i%s" % (tag.count * int(tag.dtype[0]), tag.dtype[1])
+ line = "* %i %s (%s) %s" % (
+ tag.code, tag.name, typecode, tag.as_str())
+ s.append(line[:PRINT_LINE_LEN].lstrip())
+ return '\n'.join(s)
+
+
+class FileHandle(object):
+ """Binary file handle.
+
+ * Handle embedded files (for CZI within CZI files).
+ * Allow to re-open closed files (for multi file formats such as OME-TIFF).
+ * Read numpy arrays and records from file like objects.
+
+ Only binary read, seek, tell, and close are supported on embedded files.
+ When initialized from another file handle, do not use it unless this
+ FileHandle is closed.
+
+ Attributes
+ ----------
+ name : str
+ Name of the file.
+ path : str
+ Absolute path to file.
+ size : int
+ Size of file in bytes.
+ is_file : bool
+ If True, file has a filno and can be memory mapped.
+
+ All attributes are read-only.
+
+ """
+ __slots__ = ('_fh', '_arg', '_mode', '_name', '_dir',
+ '_offset', '_size', '_close', 'is_file')
+
+ def __init__(self, arg, mode='rb', name=None, offset=None, size=None):
+ """Initialize file handle from file name or another file handle.
+
+ Parameters
+ ----------
+ arg : str, File, or FileHandle
+ File name or open file handle.
+ mode : str
+ File open mode in case 'arg' is a file name.
+ name : str
+ Optional name of file in case 'arg' is a file handle.
+ offset : int
+ Optional start position of embedded file. By default this is
+ the current file position.
+ size : int
+ Optional size of embedded file. By default this is the number
+ of bytes from the 'offset' to the end of the file.
+
+ """
+ self._fh = None
+ self._arg = arg
+ self._mode = mode
+ self._name = name
+ self._dir = ''
+ self._offset = offset
+ self._size = size
+ self._close = True
+ self.is_file = False
+ self.open()
+
+ def open(self):
+ """Open or re-open file."""
+ if self._fh:
+ return # file is open
+
+ if isinstance(self._arg, basestring):
+ # file name
+ self._arg = os.path.abspath(self._arg)
+ self._dir, self._name = os.path.split(self._arg)
+ self._fh = open(self._arg, self._mode)
+ self._close = True
+ if self._offset is None:
+ self._offset = 0
+ elif isinstance(self._arg, FileHandle):
+ # FileHandle
+ self._fh = self._arg._fh
+ if self._offset is None:
+ self._offset = 0
+ self._offset += self._arg._offset
+ self._close = False
+ if not self._name:
+ if self._offset:
+ name, ext = os.path.splitext(self._arg._name)
+ self._name = "%s@%i%s" % (name, self._offset, ext)
+ else:
+ self._name = self._arg._name
+ self._dir = self._arg._dir
+ else:
+ # open file object
+ self._fh = self._arg
+ if self._offset is None:
+ self._offset = self._arg.tell()
+ self._close = False
+ if not self._name:
+ try:
+ self._dir, self._name = os.path.split(self._fh.name)
+ except AttributeError:
+ self._name = "Unnamed stream"
+
+ if self._offset:
+ self._fh.seek(self._offset)
+
+ if self._size is None:
+ pos = self._fh.tell()
+ self._fh.seek(self._offset, 2)
+ self._size = self._fh.tell()
+ self._fh.seek(pos)
+
+ try:
+ self._fh.fileno()
+ self.is_file = True
+ except Exception:
+ self.is_file = False
+
+ def read(self, size=-1):
+ """Read 'size' bytes from file, or until EOF is reached."""
+ if size < 0 and self._offset:
+ size = self._size
+ return self._fh.read(size)
+
+ def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'):
+ """Return numpy.memmap of data stored in file."""
+ if not self.is_file:
+ raise ValueError("Can not memory map file without fileno.")
+ return numpy.memmap(self._fh, dtype=dtype, mode=mode,
+ offset=self._offset + offset,
+ shape=shape, order=order)
+
+ def read_array(self, dtype, count=-1, sep=""):
+ """Return numpy array from file.
+
+ Work around numpy issue #2230, "numpy.fromfile does not accept
+ StringIO object" https://github.com/numpy/numpy/issues/2230.
+
+ """
+ try:
+ return numpy.fromfile(self._fh, dtype, count, sep)
+ except IOError:
+ if count < 0:
+ size = self._size
+ else:
+ size = count * numpy.dtype(dtype).itemsize
+ data = self._fh.read(size)
+ return numpy.fromstring(data, dtype, count, sep)
+
+ def read_record(self, dtype, shape=1, byteorder=None):
+ """Return numpy record from file."""
+ try:
+ rec = numpy.rec.fromfile(self._fh, dtype, shape,
+ byteorder=byteorder)
+ except Exception:
+ dtype = numpy.dtype(dtype)
+ if shape is None:
+ shape = self._size // dtype.itemsize
+ size = product(sequence(shape)) * dtype.itemsize
+ data = self._fh.read(size)
+ return numpy.rec.fromstring(data, dtype, shape,
+ byteorder=byteorder)
+ return rec[0] if shape == 1 else rec
+
+ def tell(self):
+ """Return file's current position."""
+ return self._fh.tell() - self._offset
+
+ def seek(self, offset, whence=0):
+ """Set file's current position."""
+ if self._offset:
+ if whence == 0:
+ self._fh.seek(self._offset + offset, whence)
+ return
+ elif whence == 2:
+ self._fh.seek(self._offset + self._size + offset, 0)
+ return
+ self._fh.seek(offset, whence)
+
+ def close(self):
+ """Close file."""
+ if self._close and self._fh:
+ self._fh.close()
+ self._fh = None
+ self.is_file = False
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def __getattr__(self, name):
+ """Return attribute from underlying file object."""
+ if self._offset:
+ warnings.warn(
+ "FileHandle: '%s' not implemented for embedded files" % name)
+ return getattr(self._fh, name)
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def dirname(self):
+ return self._dir
+
+ @property
+ def path(self):
+ return os.path.join(self._dir, self._name)
+
+ @property
+ def size(self):
+ return self._size
+
+ @property
+ def closed(self):
+ return self._fh is None
+
+
+def read_bytes(fh, byteorder, dtype, count):
+ """Read tag data from file and return as byte string."""
+ dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1]
+ return fh.read_array(dtype, count).tostring()
+
+
+def read_numpy(fh, byteorder, dtype, count):
+ """Read tag data from file and return as numpy array."""
+ dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1]
+ return fh.read_array(dtype, count)
+
+
+def read_json(fh, byteorder, dtype, count):
+ """Read JSON tag data from file and return as object."""
+ data = fh.read(count)
+ try:
+ return json.loads(unicode(stripnull(data), 'utf-8'))
+ except ValueError:
+ warnings.warn("invalid JSON `%s`" % data)
+
+
+def read_mm_header(fh, byteorder, dtype, count):
+ """Read MM_HEADER tag from file and return as numpy.rec.array."""
+ return fh.read_record(MM_HEADER, byteorder=byteorder)
+
+
+def read_mm_stamp(fh, byteorder, dtype, count):
+ """Read MM_STAMP tag from file and return as numpy.array."""
+ return fh.read_array(byteorder+'f8', 8)
+
+
+def read_uic1tag(fh, byteorder, dtype, count, plane_count=None):
+ """Read MetaMorph STK UIC1Tag from file and return as dictionary.
+
+ Return empty dictionary if plane_count is unknown.
+
+ """
+ assert dtype in ('2I', '1I') and byteorder == '<'
+ result = {}
+ if dtype == '2I':
+ # pre MetaMorph 2.5 (not tested)
+ values = fh.read_array(' structure_size:
+ break
+ cz_lsm_info.append((name, dtype))
+ else:
+ cz_lsm_info = CZ_LSM_INFO
+
+ return fh.read_record(cz_lsm_info, byteorder=byteorder)
+
+
+def read_cz_lsm_floatpairs(fh):
+ """Read LSM sequence of float pairs from file and return as list."""
+ size = struct.unpack(' 0:
+ esize, etime, etype = struct.unpack(''}[fh.read(2)]
+ except IndexError:
+ raise ValueError("not a MicroManager TIFF file")
+
+ results = {}
+ fh.seek(8)
+ (index_header, index_offset, display_header, display_offset,
+ comments_header, comments_offset, summary_header, summary_length
+ ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32))
+
+ if summary_header != 2355492:
+ raise ValueError("invalid MicroManager summary_header")
+ results['summary'] = read_json(fh, byteorder, None, summary_length)
+
+ if index_header != 54773648:
+ raise ValueError("invalid MicroManager index_header")
+ fh.seek(index_offset)
+ header, count = struct.unpack(byteorder + "II", fh.read(8))
+ if header != 3453623:
+ raise ValueError("invalid MicroManager index_header")
+ data = struct.unpack(byteorder + "IIIII"*count, fh.read(20*count))
+ results['index_map'] = {
+ 'channel': data[::5], 'slice': data[1::5], 'frame': data[2::5],
+ 'position': data[3::5], 'offset': data[4::5]}
+
+ if display_header != 483765892:
+ raise ValueError("invalid MicroManager display_header")
+ fh.seek(display_offset)
+ header, count = struct.unpack(byteorder + "II", fh.read(8))
+ if header != 347834724:
+ raise ValueError("invalid MicroManager display_header")
+ results['display_settings'] = read_json(fh, byteorder, None, count)
+
+ if comments_header != 99384722:
+ raise ValueError("invalid MicroManager comments_header")
+ fh.seek(comments_offset)
+ header, count = struct.unpack(byteorder + "II", fh.read(8))
+ if header != 84720485:
+ raise ValueError("invalid MicroManager comments_header")
+ results['comments'] = read_json(fh, byteorder, None, count)
+
+ return results
+
+
+def imagej_metadata(data, bytecounts, byteorder):
+ """Return dict from ImageJ metadata tag value."""
+ _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252')
+
+ def read_string(data, byteorder):
+ return _str(stripnull(data[0 if byteorder == '<' else 1::2]))
+
+ def read_double(data, byteorder):
+ return struct.unpack(byteorder+('d' * (len(data) // 8)), data)
+
+ def read_bytes(data, byteorder):
+ #return struct.unpack('b' * len(data), data)
+ return numpy.fromstring(data, 'uint8')
+
+ metadata_types = { # big endian
+ b'info': ('info', read_string),
+ b'labl': ('labels', read_string),
+ b'rang': ('ranges', read_double),
+ b'luts': ('luts', read_bytes),
+ b'roi ': ('roi', read_bytes),
+ b'over': ('overlays', read_bytes)}
+ metadata_types.update( # little endian
+ dict((k[::-1], v) for k, v in metadata_types.items()))
+
+ if not bytecounts:
+ raise ValueError("no ImageJ metadata")
+
+ if not data[:4] in (b'IJIJ', b'JIJI'):
+ raise ValueError("invalid ImageJ metadata")
+
+ header_size = bytecounts[0]
+ if header_size < 12 or header_size > 804:
+ raise ValueError("invalid ImageJ metadata header size")
+
+ ntypes = (header_size - 4) // 8
+ header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8])
+ pos = 4 + ntypes * 8
+ counter = 0
+ result = {}
+ for mtype, count in zip(header[::2], header[1::2]):
+ values = []
+ name, func = metadata_types.get(mtype, (_str(mtype), read_bytes))
+ for _ in range(count):
+ counter += 1
+ pos1 = pos + bytecounts[counter]
+ values.append(func(data[pos:pos1], byteorder))
+ pos = pos1
+ result[name.strip()] = values[0] if count == 1 else values
+ return result
+
+
+def imagej_description(description):
+ """Return dict from ImageJ image_description tag."""
+ def _bool(val):
+ return {b'true': True, b'false': False}[val.lower()]
+
+ _str = str if sys.version_info[0] < 3 else lambda x: str(x, 'cp1252')
+ result = {}
+ for line in description.splitlines():
+ try:
+ key, val = line.split(b'=')
+ except Exception:
+ continue
+ key = key.strip()
+ val = val.strip()
+ for dtype in (int, float, _bool, _str):
+ try:
+ val = dtype(val)
+ break
+ except Exception:
+ pass
+ result[_str(key)] = val
+ return result
+
+
+def _replace_by(module_function, package=None, warn=False):
+ """Try replace decorated function by module.function."""
+ try:
+ from importlib import import_module
+ except ImportError:
+ warnings.warn('could not import module importlib')
+ return lambda func: func
+
+ def decorate(func, module_function=module_function, warn=warn):
+ try:
+ module, function = module_function.split('.')
+ if not package:
+ module = import_module(module)
+ else:
+ module = import_module('.' + module, package=package)
+ func, oldfunc = getattr(module, function), func
+ globals()['__old_' + func.__name__] = oldfunc
+ except Exception:
+ if warn:
+ warnings.warn("failed to import %s" % module_function)
+ return func
+
+ return decorate
+
+
+def decodejpg(encoded, tables=b'', photometric=None,
+ ycbcr_subsampling=None, ycbcr_positioning=None):
+ """Decode JPEG encoded byte string (using _czifile extension module)."""
+ import _czifile
+ image = _czifile.decodejpg(encoded, tables)
+ if photometric == 'rgb' and ycbcr_subsampling and ycbcr_positioning:
+ # TODO: convert YCbCr to RGB
+ pass
+ return image.tostring()
+
+
+@_replace_by('_tifffile.decodepackbits')
+def decodepackbits(encoded):
+ """Decompress PackBits encoded byte string.
+
+ PackBits is a simple byte-oriented run-length compression scheme.
+
+ """
+ func = ord if sys.version[0] == '2' else lambda x: x
+ result = []
+ result_extend = result.extend
+ i = 0
+ try:
+ while True:
+ n = func(encoded[i]) + 1
+ i += 1
+ if n < 129:
+ result_extend(encoded[i:i+n])
+ i += n
+ elif n > 129:
+ result_extend(encoded[i:i+1] * (258-n))
+ i += 1
+ except IndexError:
+ pass
+ return b''.join(result) if sys.version[0] == '2' else bytes(result)
+
+
+@_replace_by('_tifffile.decodelzw')
+def decodelzw(encoded):
+ """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string).
+
+ The strip must begin with a CLEAR code and end with an EOI code.
+
+ This is an implementation of the LZW decoding algorithm described in (1).
+ It is not compatible with old style LZW compressed files like quad-lzw.tif.
+
+ """
+ len_encoded = len(encoded)
+ bitcount_max = len_encoded * 8
+ unpack = struct.unpack
+
+ if sys.version[0] == '2':
+ newtable = [chr(i) for i in range(256)]
+ else:
+ newtable = [bytes([i]) for i in range(256)]
+ newtable.extend((0, 0))
+
+ def next_code():
+ """Return integer of `bitw` bits at `bitcount` position in encoded."""
+ start = bitcount // 8
+ s = encoded[start:start+4]
+ try:
+ code = unpack('>I', s)[0]
+ except Exception:
+ code = unpack('>I', s + b'\x00'*(4-len(s)))[0]
+ code <<= bitcount % 8
+ code &= mask
+ return code >> shr
+
+ switchbitch = { # code: bit-width, shr-bits, bit-mask
+ 255: (9, 23, int(9*'1'+'0'*23, 2)),
+ 511: (10, 22, int(10*'1'+'0'*22, 2)),
+ 1023: (11, 21, int(11*'1'+'0'*21, 2)),
+ 2047: (12, 20, int(12*'1'+'0'*20, 2)), }
+ bitw, shr, mask = switchbitch[255]
+ bitcount = 0
+
+ if len_encoded < 4:
+ raise ValueError("strip must be at least 4 characters long")
+
+ if next_code() != 256:
+ raise ValueError("strip must begin with CLEAR code")
+
+ code = 0
+ oldcode = 0
+ result = []
+ result_append = result.append
+ while True:
+ code = next_code() # ~5% faster when inlining this function
+ bitcount += bitw
+ if code == 257 or bitcount >= bitcount_max: # EOI
+ break
+ if code == 256: # CLEAR
+ table = newtable[:]
+ table_append = table.append
+ lentable = 258
+ bitw, shr, mask = switchbitch[255]
+ code = next_code()
+ bitcount += bitw
+ if code == 257: # EOI
+ break
+ result_append(table[code])
+ else:
+ if code < lentable:
+ decoded = table[code]
+ newcode = table[oldcode] + decoded[:1]
+ else:
+ newcode = table[oldcode]
+ newcode += newcode[:1]
+ decoded = newcode
+ result_append(decoded)
+ table_append(newcode)
+ lentable += 1
+ oldcode = code
+ if lentable in switchbitch:
+ bitw, shr, mask = switchbitch[lentable]
+
+ if code != 257:
+ warnings.warn("unexpected end of lzw stream (code %i)" % code)
+
+ return b''.join(result)
+
+
+@_replace_by('_tifffile.unpackints')
+def unpackints(data, dtype, itemsize, runlen=0):
+ """Decompress byte string to array of integers of any bit size <= 32.
+
+ Parameters
+ ----------
+ data : byte str
+ Data to decompress.
+ dtype : numpy.dtype or str
+ A numpy boolean or integer type.
+ itemsize : int
+ Number of bits per integer.
+ runlen : int
+ Number of consecutive integers, after which to start at next byte.
+
+ """
+ if itemsize == 1: # bitarray
+ data = numpy.fromstring(data, '|B')
+ data = numpy.unpackbits(data)
+ if runlen % 8:
+ data = data.reshape(-1, runlen + (8 - runlen % 8))
+ data = data[:, :runlen].reshape(-1)
+ return data.astype(dtype)
+
+ dtype = numpy.dtype(dtype)
+ if itemsize in (8, 16, 32, 64):
+ return numpy.fromstring(data, dtype)
+ if itemsize < 1 or itemsize > 32:
+ raise ValueError("itemsize out of range: %i" % itemsize)
+ if dtype.kind not in "biu":
+ raise ValueError("invalid dtype")
+
+ itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize)
+ if itembytes != dtype.itemsize:
+ raise ValueError("dtype.itemsize too small")
+ if runlen == 0:
+ runlen = len(data) // itembytes
+ skipbits = runlen*itemsize % 8
+ if skipbits:
+ skipbits = 8 - skipbits
+ shrbits = itembytes*8 - itemsize
+ bitmask = int(itemsize*'1'+'0'*shrbits, 2)
+ dtypestr = '>' + dtype.char # dtype always big endian?
+
+ unpack = struct.unpack
+ l = runlen * (len(data)*8 // (runlen*itemsize + skipbits))
+ result = numpy.empty((l, ), dtype)
+ bitcount = 0
+ for i in range(len(result)):
+ start = bitcount // 8
+ s = data[start:start+itembytes]
+ try:
+ code = unpack(dtypestr, s)[0]
+ except Exception:
+ code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0]
+ code <<= bitcount % 8
+ code &= bitmask
+ result[i] = code >> shrbits
+ bitcount += itemsize
+ if (i+1) % runlen == 0:
+ bitcount += skipbits
+ return result
+
+
+def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff)
+ >>> print(unpackrgb(data, '>> print(unpackrgb(data, '>> print(unpackrgb(data, '= bits)
+ data = numpy.fromstring(data, dtype.byteorder+dt)
+ result = numpy.empty((data.size, len(bitspersample)), dtype.char)
+ for i, bps in enumerate(bitspersample):
+ t = data >> int(numpy.sum(bitspersample[i+1:]))
+ t &= int('0b'+'1'*bps, 2)
+ if rescale:
+ o = ((dtype.itemsize * 8) // bps + 1) * bps
+ if o > data.dtype.itemsize * 8:
+ t = t.astype('I')
+ t *= (2**o - 1) // (2**bps - 1)
+ t //= 2**(o - (dtype.itemsize * 8))
+ result[:, i] = t
+ return result.reshape(-1)
+
+
+def reorient(image, orientation):
+ """Return reoriented view of image array.
+
+ Parameters
+ ----------
+ image : numpy array
+ Non-squeezed output of asarray() functions.
+ Axes -3 and -2 must be image length and width respectively.
+ orientation : int or str
+ One of TIFF_ORIENTATIONS keys or values.
+
+ """
+ o = TIFF_ORIENTATIONS.get(orientation, orientation)
+ if o == 'top_left':
+ return image
+ elif o == 'top_right':
+ return image[..., ::-1, :]
+ elif o == 'bottom_left':
+ return image[..., ::-1, :, :]
+ elif o == 'bottom_right':
+ return image[..., ::-1, ::-1, :]
+ elif o == 'left_top':
+ return numpy.swapaxes(image, -3, -2)
+ elif o == 'right_top':
+ return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
+ elif o == 'left_bottom':
+ return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
+ elif o == 'right_bottom':
+ return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
+
+
+def squeeze_axes(shape, axes, skip='XY'):
+ """Return shape and axes with single-dimensional entries removed.
+
+ Remove unused dimensions unless their axes are listed in 'skip'.
+
+ >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
+ ((5, 2, 1), 'TYX')
+
+ """
+ if len(shape) != len(axes):
+ raise ValueError("dimensions of axes and shape don't match")
+ shape, axes = zip(*(i for i in zip(shape, axes)
+ if i[0] > 1 or i[1] in skip))
+ return shape, ''.join(axes)
+
+
+def transpose_axes(data, axes, asaxes='CTZYX'):
+ """Return data with its axes permuted to match specified axes.
+
+ A view is returned if possible.
+
+ >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
+ (5, 2, 1, 3, 4)
+
+ """
+ for ax in axes:
+ if ax not in asaxes:
+ raise ValueError("unknown axis %s" % ax)
+ # add missing axes to data
+ shape = data.shape
+ for ax in reversed(asaxes):
+ if ax not in axes:
+ axes = ax + axes
+ shape = (1,) + shape
+ data = data.reshape(shape)
+ # transpose axes
+ data = data.transpose([axes.index(ax) for ax in asaxes])
+ return data
+
+
+def stack_pages(pages, memmap=False, *args, **kwargs):
+ """Read data from sequence of TiffPage and stack them vertically.
+
+ If memmap is True, return an array stored in a binary file on disk.
+ Additional parameters are passsed to the page asarray function.
+
+ """
+ if len(pages) == 0:
+ raise ValueError("no pages")
+
+ if len(pages) == 1:
+ return pages[0].asarray(memmap=memmap, *args, **kwargs)
+
+ result = pages[0].asarray(*args, **kwargs)
+ shape = (len(pages),) + result.shape
+ if memmap:
+ with tempfile.NamedTemporaryFile() as fh:
+ result = numpy.memmap(fh, dtype=result.dtype, shape=shape)
+ else:
+ result = numpy.empty(shape, dtype=result.dtype)
+
+ for i, page in enumerate(pages):
+ result[i] = page.asarray(*args, **kwargs)
+
+ return result
+
+
+def stripnull(string):
+ """Return string truncated at first null character.
+
+ Clean NULL terminated C strings.
+
+ >>> stripnull(b'string\\x00')
+ b'string'
+
+ """
+ i = string.find(b'\x00')
+ return string if (i < 0) else string[:i]
+
+
+def stripascii(string):
+ """Return string truncated at last byte that is 7bit ASCII.
+
+ Clean NULL separated and terminated TIFF strings.
+
+ >>> stripascii(b'string\\x00string\\n\\x01\\x00')
+ b'string\\x00string\\n'
+ >>> stripascii(b'\\x00')
+ b''
+
+ """
+ # TODO: pythonize this
+ ord_ = ord if sys.version_info[0] < 3 else lambda x: x
+ i = len(string)
+ while i:
+ i -= 1
+ if 8 < ord_(string[i]) < 127:
+ break
+ else:
+ i = -1
+ return string[:i+1]
+
+
+def format_size(size):
+ """Return file size as string from byte size."""
+ for unit in ('B', 'KB', 'MB', 'GB', 'TB'):
+ if size < 2048:
+ return "%.f %s" % (size, unit)
+ size /= 1024.0
+
+
+def sequence(value):
+ """Return tuple containing value if value is not a sequence.
+
+ >>> sequence(1)
+ (1,)
+ >>> sequence([1])
+ [1]
+
+ """
+ try:
+ len(value)
+ return value
+ except TypeError:
+ return (value, )
+
+
+def product(iterable):
+ """Return product of sequence of numbers.
+
+ Equivalent of functools.reduce(operator.mul, iterable, 1).
+
+ >>> product([2**8, 2**30])
+ 274877906944
+ >>> product([])
+ 1
+
+ """
+ prod = 1
+ for i in iterable:
+ prod *= i
+ return prod
+
+
+def natural_sorted(iterable):
+ """Return human sorted list of strings.
+
+ E.g. for sorting file names.
+
+ >>> natural_sorted(['f1', 'f2', 'f10'])
+ ['f1', 'f2', 'f10']
+
+ """
+ def sortkey(x):
+ return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
+ numbers = re.compile(r'(\d+)')
+ return sorted(iterable, key=sortkey)
+
+
+def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)):
+ """Return datetime object from timestamp in Excel serial format.
+
+ Convert LSM time stamps.
+
+ >>> excel_datetime(40237.029999999795)
+ datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
+
+ """
+ return epoch + datetime.timedelta(timestamp)
+
+
+def julian_datetime(julianday, milisecond=0):
+ """Return datetime from days since 1/1/4713 BC and ms since midnight.
+
+ Convert Julian dates according to MetaMorph.
+
+ >>> julian_datetime(2451576, 54362783)
+ datetime.datetime(2000, 2, 2, 15, 6, 2, 783)
+
+ """
+ if julianday <= 1721423:
+ # no datetime before year 1
+ return None
+
+ a = julianday + 1
+ if a > 2299160:
+ alpha = math.trunc((a - 1867216.25) / 36524.25)
+ a += 1 + alpha - alpha // 4
+ b = a + (1524 if a > 1721423 else 1158)
+ c = math.trunc((b - 122.1) / 365.25)
+ d = math.trunc(365.25 * c)
+ e = math.trunc((b - d) / 30.6001)
+
+ day = b - d - math.trunc(30.6001 * e)
+ month = e - (1 if e < 13.5 else 13)
+ year = c - (4716 if month > 2.5 else 4715)
+
+ hour, milisecond = divmod(milisecond, 1000 * 60 * 60)
+ minute, milisecond = divmod(milisecond, 1000 * 60)
+ second, milisecond = divmod(milisecond, 1000)
+
+ return datetime.datetime(year, month, day,
+ hour, minute, second, milisecond)
+
+
+def test_tifffile(directory='testimages', verbose=True):
+ """Read all images in directory.
+
+ Print error message on failure.
+
+ >>> test_tifffile(verbose=False)
+
+ """
+ successful = 0
+ failed = 0
+ start = time.time()
+ for f in glob.glob(os.path.join(directory, '*.*')):
+ if verbose:
+ print("\n%s>\n" % f.lower(), end='')
+ t0 = time.time()
+ try:
+ tif = TiffFile(f, multifile=True)
+ except Exception as e:
+ if not verbose:
+ print(f, end=' ')
+ print("ERROR:", e)
+ failed += 1
+ continue
+ try:
+ img = tif.asarray()
+ except ValueError:
+ try:
+ img = tif[0].asarray()
+ except Exception as e:
+ if not verbose:
+ print(f, end=' ')
+ print("ERROR:", e)
+ failed += 1
+ continue
+ finally:
+ tif.close()
+ successful += 1
+ if verbose:
+ print("%s, %s %s, %s, %.0f ms" % (
+ str(tif), str(img.shape), img.dtype, tif[0].compression,
+ (time.time()-t0) * 1e3))
+ if verbose:
+ print("\nSuccessfully read %i of %i files in %.3f s\n" % (
+ successful, successful+failed, time.time()-start))
+
+
+class TIFF_SUBFILE_TYPES(object):
+ def __getitem__(self, key):
+ result = []
+ if key & 1:
+ result.append('reduced_image')
+ if key & 2:
+ result.append('page')
+ if key & 4:
+ result.append('mask')
+ return tuple(result)
+
+
+TIFF_PHOTOMETRICS = {
+ 0: 'miniswhite',
+ 1: 'minisblack',
+ 2: 'rgb',
+ 3: 'palette',
+ 4: 'mask',
+ 5: 'separated', # CMYK
+ 6: 'ycbcr',
+ 8: 'cielab',
+ 9: 'icclab',
+ 10: 'itulab',
+ 32803: 'cfa', # Color Filter Array
+ 32844: 'logl',
+ 32845: 'logluv',
+ 34892: 'linear_raw'
+}
+
+TIFF_COMPESSIONS = {
+ 1: None,
+ 2: 'ccittrle',
+ 3: 'ccittfax3',
+ 4: 'ccittfax4',
+ 5: 'lzw',
+ 6: 'ojpeg',
+ 7: 'jpeg',
+ 8: 'adobe_deflate',
+ 9: 't85',
+ 10: 't43',
+ 32766: 'next',
+ 32771: 'ccittrlew',
+ 32773: 'packbits',
+ 32809: 'thunderscan',
+ 32895: 'it8ctpad',
+ 32896: 'it8lw',
+ 32897: 'it8mp',
+ 32898: 'it8bl',
+ 32908: 'pixarfilm',
+ 32909: 'pixarlog',
+ 32946: 'deflate',
+ 32947: 'dcs',
+ 34661: 'jbig',
+ 34676: 'sgilog',
+ 34677: 'sgilog24',
+ 34712: 'jp2000',
+ 34713: 'nef',
+}
+
+TIFF_DECOMPESSORS = {
+ None: lambda x: x,
+ 'adobe_deflate': zlib.decompress,
+ 'deflate': zlib.decompress,
+ 'packbits': decodepackbits,
+ 'lzw': decodelzw,
+ # 'jpeg': decodejpg
+}
+
+TIFF_DATA_TYPES = {
+ 1: '1B', # BYTE 8-bit unsigned integer.
+ 2: '1s', # ASCII 8-bit byte that contains a 7-bit ASCII code;
+ # the last byte must be NULL (binary zero).
+ 3: '1H', # SHORT 16-bit (2-byte) unsigned integer
+ 4: '1I', # LONG 32-bit (4-byte) unsigned integer.
+ 5: '2I', # RATIONAL Two LONGs: the first represents the numerator of
+ # a fraction; the second, the denominator.
+ 6: '1b', # SBYTE An 8-bit signed (twos-complement) integer.
+ 7: '1s', # UNDEFINED An 8-bit byte that may contain anything,
+ # depending on the definition of the field.
+ 8: '1h', # SSHORT A 16-bit (2-byte) signed (twos-complement) integer.
+ 9: '1i', # SLONG A 32-bit (4-byte) signed (twos-complement) integer.
+ 10: '2i', # SRATIONAL Two SLONGs: the first represents the numerator
+ # of a fraction, the second the denominator.
+ 11: '1f', # FLOAT Single precision (4-byte) IEEE format.
+ 12: '1d', # DOUBLE Double precision (8-byte) IEEE format.
+ 13: '1I', # IFD unsigned 4 byte IFD offset.
+ #14: '', # UNICODE
+ #15: '', # COMPLEX
+ 16: '1Q', # LONG8 unsigned 8 byte integer (BigTiff)
+ 17: '1q', # SLONG8 signed 8 byte integer (BigTiff)
+ 18: '1Q', # IFD8 unsigned 8 byte IFD offset (BigTiff)
+}
+
+TIFF_SAMPLE_FORMATS = {
+ 1: 'uint',
+ 2: 'int',
+ 3: 'float',
+ #4: 'void',
+ #5: 'complex_int',
+ 6: 'complex',
+}
+
+TIFF_SAMPLE_DTYPES = {
+ ('uint', 1): '?', # bitmap
+ ('uint', 2): 'B',
+ ('uint', 3): 'B',
+ ('uint', 4): 'B',
+ ('uint', 5): 'B',
+ ('uint', 6): 'B',
+ ('uint', 7): 'B',
+ ('uint', 8): 'B',
+ ('uint', 9): 'H',
+ ('uint', 10): 'H',
+ ('uint', 11): 'H',
+ ('uint', 12): 'H',
+ ('uint', 13): 'H',
+ ('uint', 14): 'H',
+ ('uint', 15): 'H',
+ ('uint', 16): 'H',
+ ('uint', 17): 'I',
+ ('uint', 18): 'I',
+ ('uint', 19): 'I',
+ ('uint', 20): 'I',
+ ('uint', 21): 'I',
+ ('uint', 22): 'I',
+ ('uint', 23): 'I',
+ ('uint', 24): 'I',
+ ('uint', 25): 'I',
+ ('uint', 26): 'I',
+ ('uint', 27): 'I',
+ ('uint', 28): 'I',
+ ('uint', 29): 'I',
+ ('uint', 30): 'I',
+ ('uint', 31): 'I',
+ ('uint', 32): 'I',
+ ('uint', 64): 'Q',
+ ('int', 8): 'b',
+ ('int', 16): 'h',
+ ('int', 32): 'i',
+ ('int', 64): 'q',
+ ('float', 16): 'e',
+ ('float', 32): 'f',
+ ('float', 64): 'd',
+ ('complex', 64): 'F',
+ ('complex', 128): 'D',
+ ('uint', (5, 6, 5)): 'B',
+}
+
+TIFF_ORIENTATIONS = {
+ 1: 'top_left',
+ 2: 'top_right',
+ 3: 'bottom_right',
+ 4: 'bottom_left',
+ 5: 'left_top',
+ 6: 'right_top',
+ 7: 'right_bottom',
+ 8: 'left_bottom',
+}
+
+# TODO: is there a standard for character axes labels?
+AXES_LABELS = {
+ 'X': 'width',
+ 'Y': 'height',
+ 'Z': 'depth',
+ 'S': 'sample', # rgb(a)
+ 'I': 'series', # general sequence, plane, page, IFD
+ 'T': 'time',
+ 'C': 'channel', # color, emission wavelength
+ 'A': 'angle',
+ 'P': 'phase', # formerly F # P is Position in LSM!
+ 'R': 'tile', # region, point, mosaic
+ 'H': 'lifetime', # histogram
+ 'E': 'lambda', # excitation wavelength
+ 'L': 'exposure', # lux
+ 'V': 'event',
+ 'Q': 'other',
+ #'M': 'mosaic', # LSM 6
+}
+
+AXES_LABELS.update(dict((v, k) for k, v in AXES_LABELS.items()))
+
+# Map OME pixel types to numpy dtype
+OME_PIXEL_TYPES = {
+ 'int8': 'i1',
+ 'int16': 'i2',
+ 'int32': 'i4',
+ 'uint8': 'u1',
+ 'uint16': 'u2',
+ 'uint32': 'u4',
+ 'float': 'f4',
+ # 'bit': 'bit',
+ 'double': 'f8',
+ 'complex': 'c8',
+ 'double-complex': 'c16',
+}
+
+# NIH Image PicHeader v1.63
+NIH_IMAGE_HEADER = [
+ ('fileid', 'a8'),
+ ('nlines', 'i2'),
+ ('pixelsperline', 'i2'),
+ ('version', 'i2'),
+ ('oldlutmode', 'i2'),
+ ('oldncolors', 'i2'),
+ ('colors', 'u1', (3, 32)),
+ ('oldcolorstart', 'i2'),
+ ('colorwidth', 'i2'),
+ ('extracolors', 'u2', (6, 3)),
+ ('nextracolors', 'i2'),
+ ('foregroundindex', 'i2'),
+ ('backgroundindex', 'i2'),
+ ('xscale', 'f8'),
+ ('_x0', 'i2'),
+ ('_x1', 'i2'),
+ ('units_t', 'i2'), # NIH_UNITS_TYPE
+ ('p1', [('x', 'i2'), ('y', 'i2')]),
+ ('p2', [('x', 'i2'), ('y', 'i2')]),
+ ('curvefit_t', 'i2'), # NIH_CURVEFIT_TYPE
+ ('ncoefficients', 'i2'),
+ ('coeff', 'f8', 6),
+ ('_um_len', 'u1'),
+ ('um', 'a15'),
+ ('_x2', 'u1'),
+ ('binarypic', 'b1'),
+ ('slicestart', 'i2'),
+ ('sliceend', 'i2'),
+ ('scalemagnification', 'f4'),
+ ('nslices', 'i2'),
+ ('slicespacing', 'f4'),
+ ('currentslice', 'i2'),
+ ('frameinterval', 'f4'),
+ ('pixelaspectratio', 'f4'),
+ ('colorstart', 'i2'),
+ ('colorend', 'i2'),
+ ('ncolors', 'i2'),
+ ('fill1', '3u2'),
+ ('fill2', '3u2'),
+ ('colortable_t', 'u1'), # NIH_COLORTABLE_TYPE
+ ('lutmode_t', 'u1'), # NIH_LUTMODE_TYPE
+ ('invertedtable', 'b1'),
+ ('zeroclip', 'b1'),
+ ('_xunit_len', 'u1'),
+ ('xunit', 'a11'),
+ ('stacktype_t', 'i2'), # NIH_STACKTYPE_TYPE
+]
+
+NIH_COLORTABLE_TYPE = (
+ 'CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32', 'Rainbow',
+ 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum')
+
+NIH_LUTMODE_TYPE = (
+ 'PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale',
+ 'ColorLut', 'CustomGrayscale')
+
+NIH_CURVEFIT_TYPE = (
+ 'StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit',
+ 'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1', 'Uncalibrated',
+ 'UncalibratedOD')
+
+NIH_UNITS_TYPE = (
+ 'Nanometers', 'Micrometers', 'Millimeters', 'Centimeters', 'Meters',
+ 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels', 'OtherUnits')
+
+NIH_STACKTYPE_TYPE = (
+ 'VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack')
+
+# Map Universal Imaging Corporation MetaMorph internal tag ids to name and type
+UIC_TAGS = {
+ 0: ('auto_scale', int),
+ 1: ('min_scale', int),
+ 2: ('max_scale', int),
+ 3: ('spatial_calibration', int),
+ 4: ('x_calibration', Fraction),
+ 5: ('y_calibration', Fraction),
+ 6: ('calibration_units', str),
+ 7: ('name', str),
+ 8: ('thresh_state', int),
+ 9: ('thresh_state_red', int),
+ 10: ('tagid_10', None), # undefined
+ 11: ('thresh_state_green', int),
+ 12: ('thresh_state_blue', int),
+ 13: ('thresh_state_lo', int),
+ 14: ('thresh_state_hi', int),
+ 15: ('zoom', int),
+ 16: ('create_time', julian_datetime),
+ 17: ('last_saved_time', julian_datetime),
+ 18: ('current_buffer', int),
+ 19: ('gray_fit', None),
+ 20: ('gray_point_count', None),
+ 21: ('gray_x', Fraction),
+ 22: ('gray_y', Fraction),
+ 23: ('gray_min', Fraction),
+ 24: ('gray_max', Fraction),
+ 25: ('gray_unit_name', str),
+ 26: ('standard_lut', int),
+ 27: ('wavelength', int),
+ 28: ('stage_position', '(%i,2,2)u4'), # N xy positions as fractions
+ 29: ('camera_chip_offset', '(%i,2,2)u4'), # N xy offsets as fractions
+ 30: ('overlay_mask', None),
+ 31: ('overlay_compress', None),
+ 32: ('overlay', None),
+ 33: ('special_overlay_mask', None),
+ 34: ('special_overlay_compress', None),
+ 35: ('special_overlay', None),
+ 36: ('image_property', read_uic_image_property),
+ 37: ('stage_label', '%ip'), # N str
+ 38: ('autoscale_lo_info', Fraction),
+ 39: ('autoscale_hi_info', Fraction),
+ 40: ('absolute_z', '(%i,2)u4'), # N fractions
+ 41: ('absolute_z_valid', '(%i,)u4'), # N long
+ 42: ('gamma', int),
+ 43: ('gamma_red', int),
+ 44: ('gamma_green', int),
+ 45: ('gamma_blue', int),
+ 46: ('camera_bin', int),
+ 47: ('new_lut', int),
+ 48: ('image_property_ex', None),
+ 49: ('plane_property', int),
+ 50: ('user_lut_table', '(256,3)u1'),
+ 51: ('red_autoscale_info', int),
+ 52: ('red_autoscale_lo_info', Fraction),
+ 53: ('red_autoscale_hi_info', Fraction),
+ 54: ('red_minscale_info', int),
+ 55: ('red_maxscale_info', int),
+ 56: ('green_autoscale_info', int),
+ 57: ('green_autoscale_lo_info', Fraction),
+ 58: ('green_autoscale_hi_info', Fraction),
+ 59: ('green_minscale_info', int),
+ 60: ('green_maxscale_info', int),
+ 61: ('blue_autoscale_info', int),
+ 62: ('blue_autoscale_lo_info', Fraction),
+ 63: ('blue_autoscale_hi_info', Fraction),
+ 64: ('blue_min_scale_info', int),
+ 65: ('blue_max_scale_info', int),
+ #66: ('overlay_plane_color', read_uic_overlay_plane_color),
+}
+
+
+# Olympus FluoView
+MM_DIMENSION = [
+ ('name', 'a16'),
+ ('size', 'i4'),
+ ('origin', 'f8'),
+ ('resolution', 'f8'),
+ ('unit', 'a64'),
+]
+
+MM_HEADER = [
+ ('header_flag', 'i2'),
+ ('image_type', 'u1'),
+ ('image_name', 'a257'),
+ ('offset_data', 'u4'),
+ ('palette_size', 'i4'),
+ ('offset_palette0', 'u4'),
+ ('offset_palette1', 'u4'),
+ ('comment_size', 'i4'),
+ ('offset_comment', 'u4'),
+ ('dimensions', MM_DIMENSION, 10),
+ ('offset_position', 'u4'),
+ ('map_type', 'i2'),
+ ('map_min', 'f8'),
+ ('map_max', 'f8'),
+ ('min_value', 'f8'),
+ ('max_value', 'f8'),
+ ('offset_map', 'u4'),
+ ('gamma', 'f8'),
+ ('offset', 'f8'),
+ ('gray_channel', MM_DIMENSION),
+ ('offset_thumbnail', 'u4'),
+ ('voice_field', 'i4'),
+ ('offset_voice_field', 'u4'),
+]
+
+# Carl Zeiss LSM
+CZ_LSM_INFO = [
+ ('magic_number', 'u4'),
+ ('structure_size', 'i4'),
+ ('dimension_x', 'i4'),
+ ('dimension_y', 'i4'),
+ ('dimension_z', 'i4'),
+ ('dimension_channels', 'i4'),
+ ('dimension_time', 'i4'),
+ ('data_type', 'i4'), # CZ_DATA_TYPES
+ ('thumbnail_x', 'i4'),
+ ('thumbnail_y', 'i4'),
+ ('voxel_size_x', 'f8'),
+ ('voxel_size_y', 'f8'),
+ ('voxel_size_z', 'f8'),
+ ('origin_x', 'f8'),
+ ('origin_y', 'f8'),
+ ('origin_z', 'f8'),
+ ('scan_type', 'u2'),
+ ('spectral_scan', 'u2'),
+ ('type_of_data', 'u4'), # CZ_TYPE_OF_DATA
+ ('offset_vector_overlay', 'u4'),
+ ('offset_input_lut', 'u4'),
+ ('offset_output_lut', 'u4'),
+ ('offset_channel_colors', 'u4'),
+ ('time_interval', 'f8'),
+ ('offset_channel_data_types', 'u4'),
+ ('offset_scan_info', 'u4'), # CZ_LSM_SCAN_INFO
+ ('offset_ks_data', 'u4'),
+ ('offset_time_stamps', 'u4'),
+ ('offset_event_list', 'u4'),
+ ('offset_roi', 'u4'),
+ ('offset_bleach_roi', 'u4'),
+ ('offset_next_recording', 'u4'),
+ # LSM 2.0 ends here
+ ('display_aspect_x', 'f8'),
+ ('display_aspect_y', 'f8'),
+ ('display_aspect_z', 'f8'),
+ ('display_aspect_time', 'f8'),
+ ('offset_mean_of_roi_overlay', 'u4'),
+ ('offset_topo_isoline_overlay', 'u4'),
+ ('offset_topo_profile_overlay', 'u4'),
+ ('offset_linescan_overlay', 'u4'),
+ ('offset_toolbar_flags', 'u4'),
+ ('offset_channel_wavelength', 'u4'),
+ ('offset_channel_factors', 'u4'),
+ ('objective_sphere_correction', 'f8'),
+ ('offset_unmix_parameters', 'u4'),
+ # LSM 3.2, 4.0 end here
+ ('offset_acquisition_parameters', 'u4'),
+ ('offset_characteristics', 'u4'),
+ ('offset_palette', 'u4'),
+ ('time_difference_x', 'f8'),
+ ('time_difference_y', 'f8'),
+ ('time_difference_z', 'f8'),
+ ('internal_use_1', 'u4'),
+ ('dimension_p', 'i4'),
+ ('dimension_m', 'i4'),
+ ('dimensions_reserved', '16i4'),
+ ('offset_tile_positions', 'u4'),
+ ('reserved_1', '9u4'),
+ ('offset_positions', 'u4'),
+ ('reserved_2', '21u4'), # must be 0
+]
+
+# Import functions for LSM_INFO sub-records
+CZ_LSM_INFO_READERS = {
+ 'scan_info': read_cz_lsm_scan_info,
+ 'time_stamps': read_cz_lsm_time_stamps,
+ 'event_list': read_cz_lsm_event_list,
+ 'channel_colors': read_cz_lsm_floatpairs,
+ 'positions': read_cz_lsm_floatpairs,
+ 'tile_positions': read_cz_lsm_floatpairs,
+}
+
+# Map cz_lsm_info.scan_type to dimension order
+CZ_SCAN_TYPES = {
+ 0: 'XYZCT', # x-y-z scan
+ 1: 'XYZCT', # z scan (x-z plane)
+ 2: 'XYZCT', # line scan
+ 3: 'XYTCZ', # time series x-y
+ 4: 'XYZTC', # time series x-z
+ 5: 'XYTCZ', # time series 'Mean of ROIs'
+ 6: 'XYZTC', # time series x-y-z
+ 7: 'XYCTZ', # spline scan
+ 8: 'XYCZT', # spline scan x-z
+ 9: 'XYTCZ', # time series spline plane x-z
+ 10: 'XYZCT', # point mode
+}
+
+# Map dimension codes to cz_lsm_info attribute
+CZ_DIMENSIONS = {
+ 'X': 'dimension_x',
+ 'Y': 'dimension_y',
+ 'Z': 'dimension_z',
+ 'C': 'dimension_channels',
+ 'T': 'dimension_time',
+}
+
+# Description of cz_lsm_info.data_type
+CZ_DATA_TYPES = {
+ 0: 'varying data types',
+ 1: '8 bit unsigned integer',
+ 2: '12 bit unsigned integer',
+ 5: '32 bit float',
+}
+
+# Description of cz_lsm_info.type_of_data
+CZ_TYPE_OF_DATA = {
+ 0: 'Original scan data',
+ 1: 'Calculated data',
+ 2: '3D reconstruction',
+ 3: 'Topography height map',
+}
+
+CZ_LSM_SCAN_INFO_ARRAYS = {
+ 0x20000000: "tracks",
+ 0x30000000: "lasers",
+ 0x60000000: "detection_channels",
+ 0x80000000: "illumination_channels",
+ 0xa0000000: "beam_splitters",
+ 0xc0000000: "data_channels",
+ 0x11000000: "timers",
+ 0x13000000: "markers",
+}
+
+CZ_LSM_SCAN_INFO_STRUCTS = {
+ # 0x10000000: "recording",
+ 0x40000000: "track",
+ 0x50000000: "laser",
+ 0x70000000: "detection_channel",
+ 0x90000000: "illumination_channel",
+ 0xb0000000: "beam_splitter",
+ 0xd0000000: "data_channel",
+ 0x12000000: "timer",
+ 0x14000000: "marker",
+}
+
+CZ_LSM_SCAN_INFO_ATTRIBUTES = {
+ # recording
+ 0x10000001: "name",
+ 0x10000002: "description",
+ 0x10000003: "notes",
+ 0x10000004: "objective",
+ 0x10000005: "processing_summary",
+ 0x10000006: "special_scan_mode",
+ 0x10000007: "scan_type",
+ 0x10000008: "scan_mode",
+ 0x10000009: "number_of_stacks",
+ 0x1000000a: "lines_per_plane",
+ 0x1000000b: "samples_per_line",
+ 0x1000000c: "planes_per_volume",
+ 0x1000000d: "images_width",
+ 0x1000000e: "images_height",
+ 0x1000000f: "images_number_planes",
+ 0x10000010: "images_number_stacks",
+ 0x10000011: "images_number_channels",
+ 0x10000012: "linscan_xy_size",
+ 0x10000013: "scan_direction",
+ 0x10000014: "time_series",
+ 0x10000015: "original_scan_data",
+ 0x10000016: "zoom_x",
+ 0x10000017: "zoom_y",
+ 0x10000018: "zoom_z",
+ 0x10000019: "sample_0x",
+ 0x1000001a: "sample_0y",
+ 0x1000001b: "sample_0z",
+ 0x1000001c: "sample_spacing",
+ 0x1000001d: "line_spacing",
+ 0x1000001e: "plane_spacing",
+ 0x1000001f: "plane_width",
+ 0x10000020: "plane_height",
+ 0x10000021: "volume_depth",
+ 0x10000023: "nutation",
+ 0x10000034: "rotation",
+ 0x10000035: "precession",
+ 0x10000036: "sample_0time",
+ 0x10000037: "start_scan_trigger_in",
+ 0x10000038: "start_scan_trigger_out",
+ 0x10000039: "start_scan_event",
+ 0x10000040: "start_scan_time",
+ 0x10000041: "stop_scan_trigger_in",
+ 0x10000042: "stop_scan_trigger_out",
+ 0x10000043: "stop_scan_event",
+ 0x10000044: "stop_scan_time",
+ 0x10000045: "use_rois",
+ 0x10000046: "use_reduced_memory_rois",
+ 0x10000047: "user",
+ 0x10000048: "use_bc_correction",
+ 0x10000049: "position_bc_correction1",
+ 0x10000050: "position_bc_correction2",
+ 0x10000051: "interpolation_y",
+ 0x10000052: "camera_binning",
+ 0x10000053: "camera_supersampling",
+ 0x10000054: "camera_frame_width",
+ 0x10000055: "camera_frame_height",
+ 0x10000056: "camera_offset_x",
+ 0x10000057: "camera_offset_y",
+ 0x10000059: "rt_binning",
+ 0x1000005a: "rt_frame_width",
+ 0x1000005b: "rt_frame_height",
+ 0x1000005c: "rt_region_width",
+ 0x1000005d: "rt_region_height",
+ 0x1000005e: "rt_offset_x",
+ 0x1000005f: "rt_offset_y",
+ 0x10000060: "rt_zoom",
+ 0x10000061: "rt_line_period",
+ 0x10000062: "prescan",
+ 0x10000063: "scan_direction_z",
+ # track
+ 0x40000001: "multiplex_type", # 0 after line; 1 after frame
+ 0x40000002: "multiplex_order",
+ 0x40000003: "sampling_mode", # 0 sample; 1 line average; 2 frame average
+ 0x40000004: "sampling_method", # 1 mean; 2 sum
+ 0x40000005: "sampling_number",
+ 0x40000006: "acquire",
+ 0x40000007: "sample_observation_time",
+ 0x4000000b: "time_between_stacks",
+ 0x4000000c: "name",
+ 0x4000000d: "collimator1_name",
+ 0x4000000e: "collimator1_position",
+ 0x4000000f: "collimator2_name",
+ 0x40000010: "collimator2_position",
+ 0x40000011: "is_bleach_track",
+ 0x40000012: "is_bleach_after_scan_number",
+ 0x40000013: "bleach_scan_number",
+ 0x40000014: "trigger_in",
+ 0x40000015: "trigger_out",
+ 0x40000016: "is_ratio_track",
+ 0x40000017: "bleach_count",
+ 0x40000018: "spi_center_wavelength",
+ 0x40000019: "pixel_time",
+ 0x40000021: "condensor_frontlens",
+ 0x40000023: "field_stop_value",
+ 0x40000024: "id_condensor_aperture",
+ 0x40000025: "condensor_aperture",
+ 0x40000026: "id_condensor_revolver",
+ 0x40000027: "condensor_filter",
+ 0x40000028: "id_transmission_filter1",
+ 0x40000029: "id_transmission1",
+ 0x40000030: "id_transmission_filter2",
+ 0x40000031: "id_transmission2",
+ 0x40000032: "repeat_bleach",
+ 0x40000033: "enable_spot_bleach_pos",
+ 0x40000034: "spot_bleach_posx",
+ 0x40000035: "spot_bleach_posy",
+ 0x40000036: "spot_bleach_posz",
+ 0x40000037: "id_tubelens",
+ 0x40000038: "id_tubelens_position",
+ 0x40000039: "transmitted_light",
+ 0x4000003a: "reflected_light",
+ 0x4000003b: "simultan_grab_and_bleach",
+ 0x4000003c: "bleach_pixel_time",
+ # laser
+ 0x50000001: "name",
+ 0x50000002: "acquire",
+ 0x50000003: "power",
+ # detection_channel
+ 0x70000001: "integration_mode",
+ 0x70000002: "special_mode",
+ 0x70000003: "detector_gain_first",
+ 0x70000004: "detector_gain_last",
+ 0x70000005: "amplifier_gain_first",
+ 0x70000006: "amplifier_gain_last",
+ 0x70000007: "amplifier_offs_first",
+ 0x70000008: "amplifier_offs_last",
+ 0x70000009: "pinhole_diameter",
+ 0x7000000a: "counting_trigger",
+ 0x7000000b: "acquire",
+ 0x7000000c: "point_detector_name",
+ 0x7000000d: "amplifier_name",
+ 0x7000000e: "pinhole_name",
+ 0x7000000f: "filter_set_name",
+ 0x70000010: "filter_name",
+ 0x70000013: "integrator_name",
+ 0x70000014: "channel_name",
+ 0x70000015: "detector_gain_bc1",
+ 0x70000016: "detector_gain_bc2",
+ 0x70000017: "amplifier_gain_bc1",
+ 0x70000018: "amplifier_gain_bc2",
+ 0x70000019: "amplifier_offset_bc1",
+ 0x70000020: "amplifier_offset_bc2",
+ 0x70000021: "spectral_scan_channels",
+ 0x70000022: "spi_wavelength_start",
+ 0x70000023: "spi_wavelength_stop",
+ 0x70000026: "dye_name",
+ 0x70000027: "dye_folder",
+ # illumination_channel
+ 0x90000001: "name",
+ 0x90000002: "power",
+ 0x90000003: "wavelength",
+ 0x90000004: "aquire",
+ 0x90000005: "detchannel_name",
+ 0x90000006: "power_bc1",
+ 0x90000007: "power_bc2",
+ # beam_splitter
+ 0xb0000001: "filter_set",
+ 0xb0000002: "filter",
+ 0xb0000003: "name",
+ # data_channel
+ 0xd0000001: "name",
+ 0xd0000003: "acquire",
+ 0xd0000004: "color",
+ 0xd0000005: "sample_type",
+ 0xd0000006: "bits_per_sample",
+ 0xd0000007: "ratio_type",
+ 0xd0000008: "ratio_track1",
+ 0xd0000009: "ratio_track2",
+ 0xd000000a: "ratio_channel1",
+ 0xd000000b: "ratio_channel2",
+ 0xd000000c: "ratio_const1",
+ 0xd000000d: "ratio_const2",
+ 0xd000000e: "ratio_const3",
+ 0xd000000f: "ratio_const4",
+ 0xd0000010: "ratio_const5",
+ 0xd0000011: "ratio_const6",
+ 0xd0000012: "ratio_first_images1",
+ 0xd0000013: "ratio_first_images2",
+ 0xd0000014: "dye_name",
+ 0xd0000015: "dye_folder",
+ 0xd0000016: "spectrum",
+ 0xd0000017: "acquire",
+ # timer
+ 0x12000001: "name",
+ 0x12000002: "description",
+ 0x12000003: "interval",
+ 0x12000004: "trigger_in",
+ 0x12000005: "trigger_out",
+ 0x12000006: "activation_time",
+ 0x12000007: "activation_number",
+ # marker
+ 0x14000001: "name",
+ 0x14000002: "description",
+ 0x14000003: "trigger_in",
+ 0x14000004: "trigger_out",
+}
+
+# Map TIFF tag code to attribute name, default value, type, count, validator
+TIFF_TAGS = {
+ 254: ('new_subfile_type', 0, 4, 1, TIFF_SUBFILE_TYPES()),
+ 255: ('subfile_type', None, 3, 1,
+ {0: 'undefined', 1: 'image', 2: 'reduced_image', 3: 'page'}),
+ 256: ('image_width', None, 4, 1, None),
+ 257: ('image_length', None, 4, 1, None),
+ 258: ('bits_per_sample', 1, 3, 1, None),
+ 259: ('compression', 1, 3, 1, TIFF_COMPESSIONS),
+ 262: ('photometric', None, 3, 1, TIFF_PHOTOMETRICS),
+ 266: ('fill_order', 1, 3, 1, {1: 'msb2lsb', 2: 'lsb2msb'}),
+ 269: ('document_name', None, 2, None, None),
+ 270: ('image_description', None, 2, None, None),
+ 271: ('make', None, 2, None, None),
+ 272: ('model', None, 2, None, None),
+ 273: ('strip_offsets', None, 4, None, None),
+ 274: ('orientation', 1, 3, 1, TIFF_ORIENTATIONS),
+ 277: ('samples_per_pixel', 1, 3, 1, None),
+ 278: ('rows_per_strip', 2**32-1, 4, 1, None),
+ 279: ('strip_byte_counts', None, 4, None, None),
+ 280: ('min_sample_value', None, 3, None, None),
+ 281: ('max_sample_value', None, 3, None, None), # 2**bits_per_sample
+ 282: ('x_resolution', None, 5, 1, None),
+ 283: ('y_resolution', None, 5, 1, None),
+ 284: ('planar_configuration', 1, 3, 1, {1: 'contig', 2: 'separate'}),
+ 285: ('page_name', None, 2, None, None),
+ 286: ('x_position', None, 5, 1, None),
+ 287: ('y_position', None, 5, 1, None),
+ 296: ('resolution_unit', 2, 4, 1, {1: 'none', 2: 'inch', 3: 'centimeter'}),
+ 297: ('page_number', None, 3, 2, None),
+ 305: ('software', None, 2, None, None),
+ 306: ('datetime', None, 2, None, None),
+ 315: ('artist', None, 2, None, None),
+ 316: ('host_computer', None, 2, None, None),
+ 317: ('predictor', 1, 3, 1, {1: None, 2: 'horizontal'}),
+ 318: ('white_point', None, 5, 2, None),
+ 319: ('primary_chromaticities', None, 5, 6, None),
+ 320: ('color_map', None, 3, None, None),
+ 322: ('tile_width', None, 4, 1, None),
+ 323: ('tile_length', None, 4, 1, None),
+ 324: ('tile_offsets', None, 4, None, None),
+ 325: ('tile_byte_counts', None, 4, None, None),
+ 338: ('extra_samples', None, 3, None,
+ {0: 'unspecified', 1: 'assocalpha', 2: 'unassalpha'}),
+ 339: ('sample_format', 1, 3, 1, TIFF_SAMPLE_FORMATS),
+ 340: ('smin_sample_value', None, None, None, None),
+ 341: ('smax_sample_value', None, None, None, None),
+ 347: ('jpeg_tables', None, 7, None, None),
+ 530: ('ycbcr_subsampling', 1, 3, 2, None),
+ 531: ('ycbcr_positioning', 1, 3, 1, None),
+ 32996: ('sgi_matteing', None, None, 1, None), # use extra_samples
+ 32996: ('sgi_datatype', None, None, 1, None), # use sample_format
+ 32997: ('image_depth', None, 4, 1, None),
+ 32998: ('tile_depth', None, 4, 1, None),
+ 33432: ('copyright', None, 1, None, None),
+ 33445: ('md_file_tag', None, 4, 1, None),
+ 33446: ('md_scale_pixel', None, 5, 1, None),
+ 33447: ('md_color_table', None, 3, None, None),
+ 33448: ('md_lab_name', None, 2, None, None),
+ 33449: ('md_sample_info', None, 2, None, None),
+ 33450: ('md_prep_date', None, 2, None, None),
+ 33451: ('md_prep_time', None, 2, None, None),
+ 33452: ('md_file_units', None, 2, None, None),
+ 33550: ('model_pixel_scale', None, 12, 3, None),
+ 33922: ('model_tie_point', None, 12, None, None),
+ 34665: ('exif_ifd', None, None, 1, None),
+ 34735: ('geo_key_directory', None, 3, None, None),
+ 34736: ('geo_double_params', None, 12, None, None),
+ 34737: ('geo_ascii_params', None, 2, None, None),
+ 34853: ('gps_ifd', None, None, 1, None),
+ 37510: ('user_comment', None, None, None, None),
+ 42112: ('gdal_metadata', None, 2, None, None),
+ 42113: ('gdal_nodata', None, 2, None, None),
+ 50289: ('mc_xy_position', None, 12, 2, None),
+ 50290: ('mc_z_position', None, 12, 1, None),
+ 50291: ('mc_xy_calibration', None, 12, 3, None),
+ 50292: ('mc_lens_lem_na_n', None, 12, 3, None),
+ 50293: ('mc_channel_name', None, 1, None, None),
+ 50294: ('mc_ex_wavelength', None, 12, 1, None),
+ 50295: ('mc_time_stamp', None, 12, 1, None),
+ 50838: ('imagej_byte_counts', None, None, None, None),
+ 65200: ('flex_xml', None, 2, None, None),
+ # code: (attribute name, default value, type, count, validator)
+}
+
+# Map custom TIFF tag codes to attribute names and import functions
+CUSTOM_TAGS = {
+ 700: ('xmp', read_bytes),
+ 34377: ('photoshop', read_numpy),
+ 33723: ('iptc', read_bytes),
+ 34675: ('icc_profile', read_bytes),
+ 33628: ('uic1tag', read_uic1tag), # Universal Imaging Corporation STK
+ 33629: ('uic2tag', read_uic2tag),
+ 33630: ('uic3tag', read_uic3tag),
+ 33631: ('uic4tag', read_uic4tag),
+ 34361: ('mm_header', read_mm_header), # Olympus FluoView
+ 34362: ('mm_stamp', read_mm_stamp),
+ 34386: ('mm_user_block', read_bytes),
+ 34412: ('cz_lsm_info', read_cz_lsm_info), # Carl Zeiss LSM
+ 43314: ('nih_image_header', read_nih_image_header),
+ # 40001: ('mc_ipwinscal', read_bytes),
+ 40100: ('mc_id_old', read_bytes),
+ 50288: ('mc_id', read_bytes),
+ 50296: ('mc_frame_properties', read_bytes),
+ 50839: ('imagej_metadata', read_bytes),
+ 51123: ('micromanager_metadata', read_json),
+}
+
+# Max line length of printed output
+PRINT_LINE_LEN = 79
+
+
+def imshow(data, title=None, vmin=0, vmax=None, cmap=None,
+ bitspersample=None, photometric='rgb', interpolation='nearest',
+ dpi=96, figure=None, subplot=111, maxdim=8192, **kwargs):
+ """Plot n-dimensional images using matplotlib.pyplot.
+
+ Return figure, subplot and plot axis.
+ Requires pyplot already imported ``from matplotlib import pyplot``.
+
+ Parameters
+ ----------
+ bitspersample : int or None
+ Number of bits per channel in integer RGB images.
+ photometric : {'miniswhite', 'minisblack', 'rgb', or 'palette'}
+ The color space of the image data.
+ title : str
+ Window and subplot title.
+ figure : matplotlib.figure.Figure (optional).
+ Matplotlib to use for plotting.
+ subplot : int
+ A matplotlib.pyplot.subplot axis.
+ maxdim : int
+ maximum image size in any dimension.
+ kwargs : optional
+ Arguments for matplotlib.pyplot.imshow.
+
+ """
+ #if photometric not in ('miniswhite', 'minisblack', 'rgb', 'palette'):
+ # raise ValueError("Can't handle %s photometrics" % photometric)
+ # TODO: handle photometric == 'separated' (CMYK)
+ isrgb = photometric in ('rgb', 'palette')
+ data = numpy.atleast_2d(data.squeeze())
+ data = data[(slice(0, maxdim), ) * len(data.shape)]
+
+ dims = data.ndim
+ if dims < 2:
+ raise ValueError("not an image")
+ elif dims == 2:
+ dims = 0
+ isrgb = False
+ else:
+ if isrgb and data.shape[-3] in (3, 4):
+ data = numpy.swapaxes(data, -3, -2)
+ data = numpy.swapaxes(data, -2, -1)
+ elif not isrgb and (data.shape[-1] < data.shape[-2] // 16 and
+ data.shape[-1] < data.shape[-3] // 16 and
+ data.shape[-1] < 5):
+ data = numpy.swapaxes(data, -3, -1)
+ data = numpy.swapaxes(data, -2, -1)
+ isrgb = isrgb and data.shape[-1] in (3, 4)
+ dims -= 3 if isrgb else 2
+
+ if photometric == 'palette' and isrgb:
+ datamax = data.max()
+ if datamax > 255:
+ data >>= 8 # possible precision loss
+ data = data.astype('B')
+ elif data.dtype.kind in 'ui':
+ if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
+ try:
+ bitspersample = int(math.ceil(math.log(data.max(), 2)))
+ except Exception:
+ bitspersample = data.dtype.itemsize * 8
+ elif not isinstance(bitspersample, int):
+ # bitspersample can be tuple, e.g. (5, 6, 5)
+ bitspersample = data.dtype.itemsize * 8
+ datamax = 2**bitspersample
+ if isrgb:
+ if bitspersample < 8:
+ data <<= 8 - bitspersample
+ elif bitspersample > 8:
+ data >>= bitspersample - 8 # precision loss
+ data = data.astype('B')
+ elif data.dtype.kind == 'f':
+ datamax = data.max()
+ if isrgb and datamax > 1.0:
+ if data.dtype.char == 'd':
+ data = data.astype('f')
+ data /= datamax
+ elif data.dtype.kind == 'b':
+ datamax = 1
+ elif data.dtype.kind == 'c':
+ raise NotImplementedError("complex type") # TODO: handle complex types
+
+ if not isrgb:
+ if vmax is None:
+ vmax = datamax
+ if vmin is None:
+ if data.dtype.kind == 'i':
+ dtmin = numpy.iinfo(data.dtype).min
+ vmin = numpy.min(data)
+ if vmin == dtmin:
+ vmin = numpy.min(data > dtmin)
+ if data.dtype.kind == 'f':
+ dtmin = numpy.finfo(data.dtype).min
+ vmin = numpy.min(data)
+ if vmin == dtmin:
+ vmin = numpy.min(data > dtmin)
+ else:
+ vmin = 0
+
+ pyplot = sys.modules['matplotlib.pyplot']
+
+ if figure is None:
+ pyplot.rc('font', family='sans-serif', weight='normal', size=8)
+ figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True,
+ facecolor='1.0', edgecolor='w')
+ try:
+ figure.canvas.manager.window.title(title)
+ except Exception:
+ pass
+ pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.9,
+ left=0.1, right=0.95, hspace=0.05, wspace=0.0)
+ subplot = pyplot.subplot(subplot)
+
+ if title:
+ try:
+ title = unicode(title, 'Windows-1252')
+ except TypeError:
+ pass
+ pyplot.title(title, size=11)
+
+ if cmap is None:
+ if data.dtype.kind in 'ubf' or vmin == 0:
+ cmap = 'cubehelix'
+ else:
+ cmap = 'coolwarm'
+ if photometric == 'miniswhite':
+ cmap += '_r'
+
+ image = pyplot.imshow(data[(0, ) * dims].squeeze(), vmin=vmin, vmax=vmax,
+ cmap=cmap, interpolation=interpolation, **kwargs)
+
+ if not isrgb:
+ pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05
+
+ def format_coord(x, y):
+ # callback function to format coordinate display in toolbar
+ x = int(x + 0.5)
+ y = int(y + 0.5)
+ try:
+ if dims:
+ return "%s @ %s [%4i, %4i]" % (cur_ax_dat[1][y, x],
+ current, x, y)
+ else:
+ return "%s @ [%4i, %4i]" % (data[y, x], x, y)
+ except IndexError:
+ return ""
+
+ pyplot.gca().format_coord = format_coord
+
+ if dims:
+ current = list((0, ) * dims)
+ cur_ax_dat = [0, data[tuple(current)].squeeze()]
+ sliders = [pyplot.Slider(
+ pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]),
+ 'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5',
+ valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)]
+ for slider in sliders:
+ slider.drawon = False
+
+ def set_image(current, sliders=sliders, data=data):
+ # change image and redraw canvas
+ cur_ax_dat[1] = data[tuple(current)].squeeze()
+ image.set_data(cur_ax_dat[1])
+ for ctrl, index in zip(sliders, current):
+ ctrl.eventson = False
+ ctrl.set_val(index)
+ ctrl.eventson = True
+ figure.canvas.draw()
+
+ def on_changed(index, axis, data=data, current=current):
+ # callback function for slider change event
+ index = int(round(index))
+ cur_ax_dat[0] = axis
+ if index == current[axis]:
+ return
+ if index >= data.shape[axis]:
+ index = 0
+ elif index < 0:
+ index = data.shape[axis] - 1
+ current[axis] = index
+ set_image(current)
+
+ def on_keypressed(event, data=data, current=current):
+ # callback function for key press event
+ key = event.key
+ axis = cur_ax_dat[0]
+ if str(key) in '0123456789':
+ on_changed(key, axis)
+ elif key == 'right':
+ on_changed(current[axis] + 1, axis)
+ elif key == 'left':
+ on_changed(current[axis] - 1, axis)
+ elif key == 'up':
+ cur_ax_dat[0] = 0 if axis == len(data.shape)-1 else axis + 1
+ elif key == 'down':
+ cur_ax_dat[0] = len(data.shape)-1 if axis == 0 else axis - 1
+ elif key == 'end':
+ on_changed(data.shape[axis] - 1, axis)
+ elif key == 'home':
+ on_changed(0, axis)
+
+ figure.canvas.mpl_connect('key_press_event', on_keypressed)
+ for axis, ctrl in enumerate(sliders):
+ ctrl.on_changed(lambda k, a=axis: on_changed(k, a))
+
+ return figure, subplot, image
+
+
+def _app_show():
+ """Block the GUI. For use as skimage plugin."""
+ pyplot = sys.modules['matplotlib.pyplot']
+ pyplot.show()
+
+
+def main(argv=None):
+ """Command line usage main function."""
+ if float(sys.version[0:3]) < 2.6:
+ print("This script requires Python version 2.6 or better.")
+ print("This is Python version %s" % sys.version)
+ return 0
+ if argv is None:
+ argv = sys.argv
+
+ import optparse
+
+ parser = optparse.OptionParser(
+ usage="usage: %prog [options] path",
+ description="Display image data in TIFF files.",
+ version="%%prog %s" % __version__)
+ opt = parser.add_option
+ opt('-p', '--page', dest='page', type='int', default=-1,
+ help="display single page")
+ opt('-s', '--series', dest='series', type='int', default=-1,
+ help="display series of pages of same shape")
+ opt('--nomultifile', dest='nomultifile', action='store_true',
+ default=False, help="don't read OME series from multiple files")
+ opt('--noplot', dest='noplot', action='store_true', default=False,
+ help="don't display images")
+ opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear',
+ help="image interpolation method")
+ opt('--dpi', dest='dpi', type='int', default=96,
+ help="set plot resolution")
+ opt('--debug', dest='debug', action='store_true', default=False,
+ help="raise exception on failures")
+ opt('--test', dest='test', action='store_true', default=False,
+ help="try read all images in path")
+ opt('--doctest', dest='doctest', action='store_true', default=False,
+ help="runs the docstring examples")
+ opt('-v', '--verbose', dest='verbose', action='store_true', default=True)
+ opt('-q', '--quiet', dest='verbose', action='store_false')
+
+ settings, path = parser.parse_args()
+ path = ' '.join(path)
+
+ if settings.doctest:
+ import doctest
+ doctest.testmod()
+ return 0
+ if not path:
+ parser.error("No file specified")
+ if settings.test:
+ test_tifffile(path, settings.verbose)
+ return 0
+
+ if any(i in path for i in '?*'):
+ path = glob.glob(path)
+ if not path:
+ print('no files match the pattern')
+ return 0
+ # TODO: handle image sequences
+ #if len(path) == 1:
+ path = path[0]
+
+ print("Reading file structure...", end=' ')
+ start = time.time()
+ try:
+ tif = TiffFile(path, multifile=not settings.nomultifile)
+ except Exception as e:
+ if settings.debug:
+ raise
+ else:
+ print("\n", e)
+ sys.exit(0)
+ print("%.3f ms" % ((time.time()-start) * 1e3))
+
+ if tif.is_ome:
+ settings.norgb = True
+
+ images = [(None, tif[0 if settings.page < 0 else settings.page])]
+ if not settings.noplot:
+ print("Reading image data... ", end=' ')
+
+ def notnone(x):
+ return next(i for i in x if i is not None)
+ start = time.time()
+ try:
+ if settings.page >= 0:
+ images = [(tif.asarray(key=settings.page),
+ tif[settings.page])]
+ elif settings.series >= 0:
+ images = [(tif.asarray(series=settings.series),
+ notnone(tif.series[settings.series].pages))]
+ else:
+ images = []
+ for i, s in enumerate(tif.series):
+ try:
+ images.append(
+ (tif.asarray(series=i), notnone(s.pages)))
+ except ValueError as e:
+ images.append((None, notnone(s.pages)))
+ if settings.debug:
+ raise
+ else:
+ print("\n* series %i failed: %s... " % (i, e),
+ end='')
+ print("%.3f ms" % ((time.time()-start) * 1e3))
+ except Exception as e:
+ if settings.debug:
+ raise
+ else:
+ print(e)
+
+ tif.close()
+
+ print("\nTIFF file:", tif)
+ print()
+ for i, s in enumerate(tif.series):
+ print ("Series %i" % i)
+ print(s)
+ print()
+ for i, page in images:
+ print(page)
+ print(page.tags)
+ if page.is_palette:
+ print("\nColor Map:", page.color_map.shape, page.color_map.dtype)
+ for attr in ('cz_lsm_info', 'cz_lsm_scan_info', 'uic_tags',
+ 'mm_header', 'imagej_tags', 'micromanager_metadata',
+ 'nih_image_header'):
+ if hasattr(page, attr):
+ print("", attr.upper(), Record(getattr(page, attr)), sep="\n")
+ print()
+ if page.is_micromanager:
+ print('MICROMANAGER_FILE_METADATA')
+ print(Record(tif.micromanager_metadata))
+
+ if images and not settings.noplot:
+ try:
+ import matplotlib
+ matplotlib.use('TkAgg')
+ from matplotlib import pyplot
+ except ImportError as e:
+ warnings.warn("failed to import matplotlib.\n%s" % e)
+ else:
+ for img, page in images:
+ if img is None:
+ continue
+ vmin, vmax = None, None
+ if 'gdal_nodata' in page.tags:
+ try:
+ vmin = numpy.min(img[img > float(page.gdal_nodata)])
+ except ValueError:
+ pass
+ if page.is_stk:
+ try:
+ vmin = page.uic_tags['min_scale']
+ vmax = page.uic_tags['max_scale']
+ except KeyError:
+ pass
+ else:
+ if vmax <= vmin:
+ vmin, vmax = None, None
+ title = "%s\n %s" % (str(tif), str(page))
+ imshow(img, title=title, vmin=vmin, vmax=vmax,
+ bitspersample=page.bits_per_sample,
+ photometric=page.photometric,
+ interpolation=settings.interpol,
+ dpi=settings.dpi)
+ pyplot.show()
+
+
+TIFFfile = TiffFile # backwards compatibility
+
+if sys.version_info[0] > 2:
+ basestring = str, bytes
+ unicode = str
+
+if __name__ == "__main__":
+ sys.exit(main())
+
diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py
index 885ce0c2..fcc26e9e 100644
--- a/skimage/io/_plugins/pil_plugin.py
+++ b/skimage/io/_plugins/pil_plugin.py
@@ -14,7 +14,7 @@ except ImportError:
from skimage.util import img_as_ubyte, img_as_uint
from six import string_types
-from tifffile import imread as tif_imread, imsave as tif_imsave
+from skimage.external.tifffile import imread as tif_imread, imsave as tif_imsave
def imread(fname, dtype=None):
diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py
index 58038db4..efdddf1b 100644
--- a/skimage/io/_plugins/tifffile_plugin.py
+++ b/skimage/io/_plugins/tifffile_plugin.py
@@ -1,6 +1 @@
-try:
- from tifffile import imread, imsave
-except ImportError:
- raise ImportError("The tifffile module could not be found.\n"
- "It can be obtained at "
- "\n")
+from skimage.external.tifffile import imread, imsave