From 36ab379e0c9fb80786d77488a2f81cb5bd224c84 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 06:08:35 -0500 Subject: [PATCH 01/25] Use the PyPI tifffile instead of bundling --- DEPENDS.txt | 1 + bento.info | 3 - requirements.txt | 1 + skimage/io/_plugins/tifffile.c | 962 ------ skimage/io/_plugins/tifffile.py | 4847 ------------------------------- skimage/io/setup.py | 4 - 6 files changed, 2 insertions(+), 5816 deletions(-) delete mode 100644 skimage/io/_plugins/tifffile.c delete mode 100644 skimage/io/_plugins/tifffile.py diff --git a/DEPENDS.txt b/DEPENDS.txt index aa3cd021..e5c10521 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -16,6 +16,7 @@ Runtime requirements * `NetworkX `__ * `Pillow `__ (or `PIL `__) +* `Tifffile `__ Known build errors ------------------ diff --git a/bento.info b/bento.info index bcec295d..564f1f5a 100644 --- a/bento.info +++ b/bento.info @@ -157,9 +157,6 @@ Library: Extension: skimage.graph._ncut_cy Sources: skimage/graph/_ncut_cy.pyx - Extension: skimage.io._plugins._tifffile - Sources: - skimage/io/_plugins/tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/requirements.txt b/requirements.txt index 98f3b77f..85f5a1f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 +tifffile>=0.1 diff --git a/skimage/io/_plugins/tifffile.c b/skimage/io/_plugins/tifffile.c deleted file mode 100644 index 6aa0eaa1..00000000 --- a/skimage/io/_plugins/tifffile.c +++ /dev/null @@ -1,962 +0,0 @@ - - -/* tifffile.c - -A Python C extension module for decoding PackBits and LZW encoded TIFF data. - -Refer to the tifffile.py module for documentation and tests. - -:Author: - `Christoph Gohlke `_ - -:Organization: - Laboratory for Fluorescence Dynamics, University of California, Irvine - -:Version: 2013.11.05 - -Install -------- -Use this Python distutils setup script to build the extension module:: - - # setup.py - # Usage: ``python setup.py build_ext --inplace`` - from distutils.core import setup, Extension - import numpy - setup(name='_tifffile', - ext_modules=[Extension('_tifffile', ['tifffile.c'], - include_dirs=[numpy.get_include()])]) - -License -------- -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. -*/ - -#define _VERSION_ "2013.11.05" - -#define WIN32_LEAN_AND_MEAN -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION - -#include "Python.h" -#include "string.h" -#include "numpy/arrayobject.h" - -/* little endian by default */ -#ifndef MSB -#define MSB 1 -#endif - -#if MSB -#define LSB 0 -#define BOC '<' -#else -#define LSB 1 -#define BOC '>' -#endif - -#define NO_ERROR 0 -#define VALUE_ERROR -1 - -#ifdef _MSC_VER -typedef unsigned __int8 uint8_t; -typedef unsigned __int16 uint16_t; -typedef unsigned __int32 uint32_t; -typedef unsigned __int64 uint64_t; -#ifdef _WIN64 -typedef __int64 ssize_t; -typedef signed __int64 intptr_t; -typedef unsigned __int64 uintptr_t; -#define SSIZE_MAX (9223372036854775808) -#else -typedef int ssize_t; -typedef _W64 signed int intptr_t; -typedef _W64 unsigned int uintptr_t; -#define SSIZE_MAX (2147483648) -#endif -#else -/* non MS compilers */ -#include -#include -#endif - -#define SWAP2BYTES(x) \ - ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) - -#define SWAP4BYTES(x) \ - ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ - (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) - -#define SWAP8BYTES(x) \ - ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ - (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ - (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ - (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) - -struct BYTE_STRING { - unsigned int ref; /* reference count */ - unsigned int len; /* length of string */ - char *str; /* pointer to bytes */ -}; - -typedef union { - uint8_t b[2]; - uint16_t i; -} u_uint16; - -typedef union { - uint8_t b[4]; - uint32_t i; -} u_uint32; - -typedef union { - uint8_t b[8]; - uint64_t i; -} u_uint64; - -/*****************************************************************************/ -/* C functions */ - -/* Return mask for itemsize bits */ -unsigned char bitmask(const int itemsize) { - unsigned char result = 0; - unsigned char power = 1; - int i; - for (i = 0; i < itemsize; i++) { - result += power; - power *= 2; - } - return result << (8 - itemsize); -} - -/** Unpack sequence of tigthly packed 1-32 bit integers. - -Native byte order will be returned. - -Input data array should be padded to the next 16, 32 or 64-bit boundary -if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). - -*/ -int unpackbits( - unsigned char *data, - const ssize_t size, /** size of data in bytes */ - const int itemsize, /** number of bits in integer */ - ssize_t numitems, /** number of items to unpack */ - unsigned char *result /** buffer to store unpacked items */ - ) -{ - ssize_t i, j, k, storagesize; - unsigned char value; - /* Input validation is done in wrapper function */ - storagesize = (ssize_t)(ceil(itemsize / 8.0)); - storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; - switch (itemsize) { - case 8: - case 16: - case 32: - case 64: - memcpy(result, data, numitems*storagesize); - return NO_ERROR; - case 1: - for (i = 0, j = 0; i < numitems/8; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(128)) >> 7; - result[j++] = (value & (unsigned char)(64)) >> 6; - result[j++] = (value & (unsigned char)(32)) >> 5; - result[j++] = (value & (unsigned char)(16)) >> 4; - result[j++] = (value & (unsigned char)(8)) >> 3; - result[j++] = (value & (unsigned char)(4)) >> 2; - result[j++] = (value & (unsigned char)(2)) >> 1; - result[j++] = (value & (unsigned char)(1)); - } - if (numitems % 8) { - value = data[i]; - switch (numitems % 8) { - case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; - case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; - case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; - case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; - case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; - case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; - case 1: result[j] = (value & (unsigned char)(128)) >> 7; - } - } - return NO_ERROR; - case 2: - for (i = 0, j = 0; i < numitems/4; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(192)) >> 6; - result[j++] = (value & (unsigned char)(48)) >> 4; - result[j++] = (value & (unsigned char)(12)) >> 2; - result[j++] = (value & (unsigned char)(3)); - } - if (numitems % 4) { - value = data[i]; - switch (numitems % 4) { - case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; - case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; - case 1: result[j] = (value & (unsigned char)(192)) >> 6; - } - } - return NO_ERROR; - case 4: - for (i = 0, j = 0; i < numitems/2; i++) { - value = data[i]; - result[j++] = (value & (unsigned char)(240)) >> 4; - result[j++] = (value & (unsigned char)(15)); - } - if (numitems % 2) { - value = data[i]; - result[j] = (value & (unsigned char)(240)) >> 4; - } - return NO_ERROR; - case 24: - j = k = 0; - for (i = 0; i < numitems; i++) { - result[j++] = 0; - result[j++] = data[k++]; - result[j++] = data[k++]; - result[j++] = data[k++]; - } - return NO_ERROR; - } - /* 3, 5, 6, 7 */ - if (itemsize < 8) { - int shr = 16; - u_uint16 value, mask, tmp; - j = k = 0; - value.b[MSB] = data[j++]; - value.b[LSB] = data[j++]; - mask.b[MSB] = bitmask(itemsize); - mask.b[LSB] = 0; - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[LSB]; - if (shr < itemsize) { - value.b[MSB] = value.b[LSB]; - value.b[LSB] = data[j++]; - mask.i <<= 8 - itemsize; - shr += 8; - } else { - mask.i >>= itemsize; - } - } - return NO_ERROR; - } - /* 9, 10, 11, 12, 13, 14, 15 */ - if (itemsize < 16) { - int shr = 32; - u_uint32 value, mask, tmp; - mask.i = 0; - j = k = 0; -#if MSB - for (i = 3; i >= 0; i--) { - value.b[i] = data[j++]; - } - mask.b[3] = 0xFF; - mask.b[2] = bitmask(itemsize-8); - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[0]; /* swap bytes */ - result[k++] = tmp.b[1]; - if (shr < itemsize) { - value.b[3] = value.b[1]; - value.b[2] = value.b[0]; - value.b[1] = data[j++]; - value.b[0] = data[j++]; - mask.i <<= 16 - itemsize; - shr += 16; - } else { - mask.i >>= itemsize; - } - } -#else - /* not implemented */ -#endif - return NO_ERROR; - } - /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ - if (itemsize < 32) { - int shr = 64; - u_uint64 value, mask, tmp; - mask.i = 0; - j = k = 0; -#if MSB - for (i = 7; i >= 0; i--) { - value.b[i] = data[j++]; - } - mask.b[7] = 0xFF; - mask.b[6] = 0xFF; - mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); - mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); - for (i = 0; i < numitems; i++) { - shr -= itemsize; - tmp.i = (value.i & mask.i) >> shr; - result[k++] = tmp.b[0]; /* swap bytes */ - result[k++] = tmp.b[1]; - result[k++] = tmp.b[2]; - result[k++] = tmp.b[3]; - if (shr < itemsize) { - value.b[7] = value.b[3]; - value.b[6] = value.b[2]; - value.b[5] = value.b[1]; - value.b[4] = value.b[0]; - value.b[3] = data[j++]; - value.b[2] = data[j++]; - value.b[1] = data[j++]; - value.b[0] = data[j++]; - mask.i <<= 32 - itemsize; - shr += 32; - } else { - mask.i >>= itemsize; - } - } -#else - /* Not implemented */ -#endif - return NO_ERROR; - } - return VALUE_ERROR; -} - -/*****************************************************************************/ -/* Python functions */ - -/** Unpack tightly packed integers. */ -char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; - -static PyObject* -py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) -{ - PyObject *byteobj = NULL; - PyArrayObject *result = NULL; - PyArray_Descr *dtype = NULL; - char *encoded = NULL; - char *decoded = NULL; - Py_ssize_t encoded_len = 0; - Py_ssize_t decoded_len = 0; - Py_ssize_t runlen = 0; - Py_ssize_t i; - int storagesize, bytesize; - int itemsize = 0; - int skipbits = 0; - static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, - &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) - return NULL; - - Py_INCREF(byteobj); - - if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { - PyErr_Format(PyExc_ValueError, "itemsize out of range"); - goto _fail; - } - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = PyBytes_GET_SIZE(byteobj); - bytesize = (int)ceil(itemsize / 8.0); - storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; - if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { - PyErr_Format(PyExc_ValueError, "data size out of range"); - goto _fail; - } - if (dtype->elsize != storagesize) { - PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); - goto _fail; - } - - if (runlen == 0) { - runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); - } - skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); - if (skipbits > 0) { - skipbits = 8 - skipbits; - } - decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / - ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); - - result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, - dtype->type_num); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); - goto _fail; - } - decoded = (char *)PyArray_DATA(result); - - for (i = 0; i < decoded_len; i+=runlen) { - if (NO_ERROR != - unpackbits((unsigned char *) encoded, - (ssize_t) encoded_len, - (int) itemsize, - (ssize_t) runlen, - (unsigned char *) decoded)) { - PyErr_Format(PyExc_ValueError, "unpackbits() failed"); - goto _fail; - } - encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + - (uint64_t)skipbits) / 8); - decoded += runlen * storagesize; - } - - if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { - switch (dtype->elsize) { - case 2: { - uint16_t *d = (uint16_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP2BYTES(*d); d++; - } - break; } - case 4: { - uint32_t *d = (uint32_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP4BYTES(*d); d++; - } - break; } - case 8: { - uint64_t *d = (uint64_t *)PyArray_DATA(result); - for (i = 0; i < PyArray_SIZE(result); i++) { - *d = SWAP8BYTES(*d); d++; - } - break; } - } - } - Py_DECREF(byteobj); - Py_DECREF(dtype); - return PyArray_Return(result); - - _fail: - Py_XDECREF(byteobj); - Py_XDECREF(result); - Py_XDECREF(dtype); - return NULL; -} - - -/** Decode TIFF PackBits encoded string. */ -char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; - -static PyObject * -py_decodepackbits(PyObject *obj, PyObject *args) -{ - int n; - char e; - char *decoded = NULL; - char *encoded = NULL; - char *encoded_end = NULL; - char *encoded_pos = NULL; - unsigned int encoded_len; - unsigned int decoded_len; - PyObject *byteobj = NULL; - PyObject *result = NULL; - - if (!PyArg_ParseTuple(args, "O", &byteobj)) - return NULL; - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - Py_INCREF(byteobj); - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); - - /* release GIL: byte/string objects are immutable */ - Py_BEGIN_ALLOW_THREADS - - /* determine size of decoded string */ - encoded_pos = encoded; - encoded_end = encoded + encoded_len; - decoded_len = 0; - while (encoded_pos < encoded_end) { - n = (int)*encoded_pos++; - if (n >= 0) { - n++; - if (encoded_pos+n > encoded_end) - n = (int)(encoded_end - encoded_pos); - encoded_pos += n; - decoded_len += n; - } else if (n > -128) { - encoded_pos++; - decoded_len += 1-n; - } - } - Py_END_ALLOW_THREADS - - result = PyBytes_FromStringAndSize(0, decoded_len); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); - goto _fail; - } - decoded = PyBytes_AS_STRING(result); - - Py_BEGIN_ALLOW_THREADS - - /* decode string */ - encoded_end = encoded + encoded_len; - while (encoded < encoded_end) { - n = (int)*encoded++; - if (n >= 0) { - n++; - if (encoded+n > encoded_end) - n = (int)(encoded_end - encoded); - /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ - while (n--) - *decoded++ = *encoded++; - } else if (n > -128) { - n = 1 - n; - e = *encoded++; - /* memset(decoded, e, n); decoded += n; */ - while (n--) - *decoded++ = e; - } - } - Py_END_ALLOW_THREADS - - Py_DECREF(byteobj); - return result; - - _fail: - Py_XDECREF(byteobj); - Py_XDECREF(result); - return NULL; -} - - -/** Decode TIFF LZW encoded string. */ -char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; - -static PyObject * -py_decodelzw(PyObject *obj, PyObject *args) -{ - PyThreadState *_save = NULL; - PyObject *byteobj = NULL; - PyObject *result = NULL; - int i, j; - unsigned int encoded_len = 0; - unsigned int decoded_len = 0; - unsigned int result_len = 0; - unsigned int table_len = 0; - unsigned int len; - unsigned int code, c, oldcode, mask, shr; - uint64_t bitcount, bitw; - char *encoded = NULL; - char *result_ptr = NULL; - char *table2 = NULL; - char *cptr; - struct BYTE_STRING *decoded = NULL; - struct BYTE_STRING *decoded_ptr = NULL; - struct BYTE_STRING *table[4096]; - struct BYTE_STRING *newentry, *newresult, *t; - int little_endian = 0; - - if (!PyArg_ParseTuple(args, "O", &byteobj)) - return NULL; - - if (!PyBytes_Check(byteobj)) { - PyErr_Format(PyExc_TypeError, "expected byte string as input"); - goto _fail; - } - - Py_INCREF(byteobj); - encoded = PyBytes_AS_STRING(byteobj); - encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); - /* - if (encoded_len >= 512 * 1024 * 1024) { - PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); - goto _fail; - } - */ - /* release GIL: byte/string objects are immutable */ - _save = PyEval_SaveThread(); - - if ((*encoded != -128) || ((*(encoded+1) & 128))) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_ValueError, - "strip must begin with CLEAR code"); - goto _fail; - } - little_endian = (*(unsigned short *)encoded) & 128; - - /* allocate buffer for codes and pointers */ - decoded_len = 0; - len = (encoded_len + encoded_len/9) * sizeof(decoded); - decoded = PyMem_Malloc(len * sizeof(void *)); - if (decoded == NULL) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); - goto _fail; - } - memset((void *)decoded, 0, len * sizeof(void *)); - decoded_ptr = decoded; - - /* cache strings of length 2 */ - cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); - if (table2 == NULL) { - PyEval_RestoreThread(_save); - PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); - goto _fail; - } - for (i = 0; i < 256; i++) { - for (j = 0; j < 256; j++) { - *cptr++ = (char)i; - *cptr++ = (char)j; - } - } - - memset(table, 0, sizeof(table)); - table_len = 258; - bitw = 9; - shr = 23; - mask = 4286578688; - bitcount = 0; - result_len = 0; - code = 0; - oldcode = 0; - - while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { - /* read next code */ - code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); - if (little_endian) - code = SWAP4BYTES(code); - code <<= (unsigned int)(bitcount % 8); - code &= mask; - code >>= shr; - bitcount += bitw; - - if (code == 257) /* end of information */ - break; - - if (code == 256) { /* clearcode */ - /* initialize table and switch to 9 bit */ - while (table_len > 258) { - t = table[--table_len]; - t->ref--; - if (t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - bitw = 9; - shr = 23; - mask = 4286578688; - - /* read next code */ - code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); - if (little_endian) - code = SWAP4BYTES(code); - code <<= bitcount % 8; - code &= mask; - code >>= shr; - bitcount += bitw; - - if (code == 257) /* end of information */ - break; - - /* decoded.append(table[code]) */ - if (code < 256) { - result_len++; - *((int *)decoded_ptr++) = code; - } else { - newresult = table[code]; - newresult->ref++; - result_len += newresult->len; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - } - } else { - if (code < table_len) { - /* code is in table */ - /* newresult = table[code]; */ - /* newentry = table[oldcode] + table[code][0] */ - /* decoded.append(newresult); table.append(newentry) */ - if (code < 256) { - c = code; - *((unsigned int *)decoded_ptr++) = code; - result_len++; - } else { - newresult = table[code]; - newresult->ref++; - c = (unsigned int) *newresult->str; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - result_len += newresult->len; - } - newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); - newentry->ref = 1; - if (oldcode < 256) { - newentry->len = 2; - newentry->str = table2 + (oldcode << 9) + - ((unsigned char)c << 1); - } else { - len = table[oldcode]->len; - newentry->len = len + 1; - newentry->str = PyMem_Malloc(newentry->len); - if (newentry->str == NULL) - break; - memmove(newentry->str, table[oldcode]->str, len); - newentry->str[len] = c; - } - table[table_len++] = newentry; - } else { - /* code is not in table */ - /* newentry = newresult = table[oldcode] + table[oldcode][0] */ - /* decoded.append(newresult); table.append(newentry) */ - newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); - newentry = newresult; - newentry->ref = 2; - if (oldcode < 256) { - newentry->len = 2; - newentry->str = table2 + 514*oldcode; - } else { - len = table[oldcode]->len; - newentry->len = len + 1; - newentry->str = PyMem_Malloc(newentry->len); - if (newentry->str == NULL) - break; - memmove(newentry->str, table[oldcode]->str, len); - newentry->str[len] = *table[oldcode]->str; - } - table[table_len++] = newentry; - *(struct BYTE_STRING **)decoded_ptr++ = newresult; - result_len += newresult->len; - } - } - oldcode = code; - /* increase bit-width if necessary */ - switch (table_len) { - case 511: - bitw = 10; - shr = 22; - mask = 4290772992; - break; - case 1023: - bitw = 11; - shr = 21; - mask = 4292870144; - break; - case 2047: - bitw = 12; - shr = 20; - mask = 4293918720; - } - } - - PyEval_RestoreThread(_save); - - if (code != 257) { - PyErr_WarnEx(NULL, - "py_decodelzw encountered unexpected end of stream", 1); - } - - /* result = ''.join(decoded) */ - decoded_len = (unsigned int)(decoded_ptr - decoded); - decoded_ptr = decoded; - result = PyBytes_FromStringAndSize(0, result_len); - if (result == NULL) { - PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); - goto _fail; - } - result_ptr = PyBytes_AS_STRING(result); - - _save = PyEval_SaveThread(); - - while (decoded_len--) { - code = *((unsigned int *)decoded_ptr); - if (code < 256) { - *result_ptr++ = (char)code; - } else { - t = *((struct BYTE_STRING **)decoded_ptr); - memmove(result_ptr, t->str, t->len); - result_ptr += t->len; - if (--t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - decoded_ptr++; - } - PyMem_Free(decoded); - - while (table_len-- > 258) { - t = table[table_len]; - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - PyMem_Free(table2); - - PyEval_RestoreThread(_save); - - Py_DECREF(byteobj); - return result; - - _fail: - if (table2 != NULL) - PyMem_Free(table2); - if (decoded != NULL) { - /* Bug? are decoded_ptr and decoded_len correct? */ - while (decoded_len--) { - code = *((unsigned int *) decoded_ptr); - if (code > 258) { - t = *((struct BYTE_STRING **) decoded_ptr); - if (--t->ref == 0) { - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - } - } - PyMem_Free(decoded); - } - while (table_len-- > 258) { - t = table[table_len]; - if (t->len > 2) - PyMem_Free(t->str); - PyMem_Free(t); - } - - Py_XDECREF(byteobj); - Py_XDECREF(result); - - return NULL; -} - -/*****************************************************************************/ -/* Create Python module */ - -char module_doc[] = - "A Python C extension module for decoding PackBits and LZW encoded " - "TIFF data.\n\n" - "Refer to the tifffile.py module for documentation and tests.\n\n" - "Authors:\n Christoph Gohlke \n" - " Laboratory for Fluorescence Dynamics, University of California, Irvine." - "\n\nVersion: %s\n"; - -static PyMethodDef module_methods[] = { -#if MSB - {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, - py_unpackints_doc}, -#endif - {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, - py_decodelzw_doc}, - {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, - py_decodepackbits_doc}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#if PY_MAJOR_VERSION >= 3 - -struct module_state { - PyObject *error; -}; - -#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) - -static int module_traverse(PyObject *m, visitproc visit, void *arg) { - Py_VISIT(GETSTATE(m)->error); - return 0; -} - -static int module_clear(PyObject *m) { - Py_CLEAR(GETSTATE(m)->error); - return 0; -} - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_tifffile", - NULL, - sizeof(struct module_state), - module_methods, - NULL, - module_traverse, - module_clear, - NULL -}; - -#define INITERROR return NULL - -PyMODINIT_FUNC -PyInit__tifffile(void) - -#else - -#define INITERROR return - -PyMODINIT_FUNC -init_tifffile(void) - -#endif -{ - PyObject *module; - - char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); - PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); - -#if PY_MAJOR_VERSION >= 3 - moduledef.m_doc = doc; - module = PyModule_Create(&moduledef); -#else - module = Py_InitModule3("_tifffile", module_methods, doc); -#endif - - PyMem_Free(doc); - - if (module == NULL) - INITERROR; - - if (_import_array() < 0) { - Py_DECREF(module); - INITERROR; - } - - { -#if PY_MAJOR_VERSION < 3 - PyObject *s = PyString_FromString(_VERSION_); -#else - PyObject *s = PyUnicode_FromString(_VERSION_); -#endif - PyObject *dict = PyModule_GetDict(module); - PyDict_SetItemString(dict, "__version__", s); - Py_DECREF(s); - } - -#if PY_MAJOR_VERSION >= 3 - return module; -#endif -} - diff --git a/skimage/io/_plugins/tifffile.py b/skimage/io/_plugins/tifffile.py deleted file mode 100644 index 11744289..00000000 --- a/skimage/io/_plugins/tifffile.py +++ /dev/null @@ -1,4847 +0,0 @@ - - -#!/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 - -try: - from . import _tifffile -except ImportError: - warnings.warn( - "failed to import the optional _tifffile C extension module.\n" - "Loading of some compressed images will be slow.\n" - "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/") - -__version__ = '2014.08.24' -__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/setup.py b/skimage/io/setup.py index dbe7e940..4e3c09c3 100644 --- a/skimage/io/setup.py +++ b/skimage/io/setup.py @@ -27,10 +27,6 @@ def configuration(parent_package='', top_path=None): sources=['_plugins/_histograms.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_plugins._tifffile', - sources=['_plugins/tifffile.c'], - include_dirs=[get_numpy_include_dirs()]) - return config if __name__ == '__main__': From 24ac72a663d8753492c2448ecce2cb97a3bc1292 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 06:30:48 -0500 Subject: [PATCH 02/25] Bump the required tifffile version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 85f5a1f5..7ce05c95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 -tifffile>=0.1 +tifffile>=0.2 From 56915d29c3e56fb1254d921d087d952a80d15c20 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 07:04:53 -0500 Subject: [PATCH 03/25] Fix tifffile imports --- skimage/io/_plugins/pil_plugin.py | 2 +- skimage/io/_plugins/tifffile_plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index a23050db..885ce0c2 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 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 fb0494c4..58038db4 100644 --- a/skimage/io/_plugins/tifffile_plugin.py +++ b/skimage/io/_plugins/tifffile_plugin.py @@ -1,5 +1,5 @@ try: - from .tifffile import imread, imsave + from tifffile import imread, imsave except ImportError: raise ImportError("The tifffile module could not be found.\n" "It can be obtained at " From 6cff910f674d9dfd9dce9c37bc856d67187a6017 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 15:12:45 -0500 Subject: [PATCH 04/25] Update the setup script to install tifffile --- tools/travis_setup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index f7602aef..c9f4c5c8 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -17,6 +17,8 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi +pip install $WHEELHOUSE numpy +pip install tifffile pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From 7d171a8e37999725ee1a8c79fcd56a488dfcfffd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:08:42 -0500 Subject: [PATCH 05/25] Fix install_requires --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f8169691..e96fe71e 100755 --- a/setup.py +++ b/setup.py @@ -142,11 +142,11 @@ if __name__ == "__main__": configuration=configuration, install_requires=[ - "six>=%s" % '.'.join(str(d) for d in DEPENDENCIES['six']) + "six>=%s" % DEPENDENCIES['six'] ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, - zip_safe=False, # the package can run out of an .egg file + zip_safe=False, # the package can run out of an .egg file entry_points={ 'console_scripts': ['skivi = skimage.scripts.skivi:main'], From c74b652e1e85476b9fea232386b4628b42c86ee9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:28:31 -0500 Subject: [PATCH 06/25] Remove install_requires altogether --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index e96fe71e..b4b74ab3 100755 --- a/setup.py +++ b/setup.py @@ -141,9 +141,6 @@ if __name__ == "__main__": ], configuration=configuration, - install_requires=[ - "six>=%s" % DEPENDENCIES['six'] - ], packages=setuptools.find_packages(exclude=['doc']), include_package_data=True, zip_safe=False, # the package can run out of an .egg file From d33833ae595810bde2ac51b0ee53ddff851449d8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 16:54:37 -0500 Subject: [PATCH 07/25] Use retries where applicable --- .travis.yml | 2 +- tools/travis_test.sh | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2b6451ca..8dc44224 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ python: - 3.4 before_install: - - tools/travis_setup.sh + - travis_retry tools/travis_setup.sh install: - python setup.py build_ext --inplace diff --git a/tools/travis_test.sh b/tools/travis_test.sh index df3a484d..3ed56785 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -ex + export DISPLAY=:99.0 export PYTHONWARNINGS="all" WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" @@ -13,17 +14,38 @@ flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples tools/header.py "Install optional dependencies" +# http://unix.stackexchange.com/a/82615 +function retry() +{ + local n=0 + local try=$3 + local cmd="${@: 1}" + [[ $# -le 1 ]] && { + echo "Usage $0 "; } + + until [[ $n -ge $try ]] + do + $cmd && break || { + echo "Command Fail.." + ((n++)) + echo "retry $n ::" + sleep 1; + } + + done +} + # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - sudo apt-get install -q python-qt4 + retry sudo apt-get install -q python-qt4 MPL_QT_API=PyQt4 MPL_DIR=$HOME/.matplotlib export QT_API=pyqt else - sudo apt-get install -q libqt4-dev - pip install PySide $WHEELHOUSE - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + retry sudo apt-get install -q libqt4-dev + retry pip install PySide $WHEELHOUSE + retry python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install MPL_QT_API=PySide MPL_DIR=$HOME/.config/matplotlib export QT_API=pyside @@ -31,20 +53,20 @@ fi # imread does NOT support py3.2 if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - pip install imread + retry sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + retry pip install imread fi # TODO: update when SimpleITK become available on py34 or hopefully pip if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - easy_install SimpleITK + retry easy_install SimpleITK fi -sudo apt-get install libfreeimage3 -pip install astropy +retry sudo apt-get install libfreeimage3 +retry pip install astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - pip install pyamg + retry pip install pyamg fi # Matplotlib settings - do not show figures during doc examples From 71233cca5155f0c6f1199c704304a0876c0d1dce Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 17:05:44 -0500 Subject: [PATCH 08/25] Break script into parts and travis_retry optional installs --- .travis.yml | 10 +++++ tools/travis_install_optional.sh | 38 ++++++++++++++++++ tools/travis_setup.sh | 2 - tools/travis_test.sh | 68 -------------------------------- 4 files changed, 48 insertions(+), 70 deletions(-) create mode 100644 tools/travis_install_optional.sh diff --git a/.travis.yml b/.travis.yml index 8dc44224..7a0e2b94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,9 @@ python: - 3.4 before_install: + - export DISPLAY=:99.0 + - export PYTHONWARNINGS="all" + - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - travis_retry tools/travis_setup.sh install: @@ -22,6 +25,13 @@ install: - python setup.py install script: + - tools/header.py "Run all tests with minimum dependencies" + - nosetests --exe -v skimage + + - tools/header.py "Pep8 and Flake tests" + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples + + - travis_retry tools/travis_install_optional.sh - tools/travis_test.sh after_success: diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh new file mode 100644 index 00000000..c8d55fdf --- /dev/null +++ b/tools/travis_install_optional.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -ex + +tools/header.py "Install optional dependencies" + +# Install Qt and then update the Matplotlib settings +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + sudo apt-get install -q python-qt4 + MPL_QT_API=PyQt4 + MPL_DIR=$HOME/.matplotlib + export QT_API=pyqt + +else + sudo apt-get install -q libqt4-dev + pip install PySide $WHEELHOUSE + python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install + MPL_QT_API=PySide + MPL_DIR=$HOME/.config/matplotlib + export QT_API=pyside +fi + +# imread does NOT support py3.2 +if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then + sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools + pip install imread +fi + +# TODO: update when SimpleITK become available on py34 or hopefully pip +if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + easy_install SimpleITK +fi + +sudo apt-get install libfreeimage3 +pip install astropy + +if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then + pip install pyamg +fi diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index c9f4c5c8..cefe3a73 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -4,8 +4,6 @@ set -ex sh -e /etc/init.d/xvfb start sudo apt-get update -WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - pip install wheel flake8 coveralls nose pip uninstall -y numpy diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 3ed56785..3d35a9b1 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,74 +1,6 @@ #!/usr/bin/env bash set -ex - -export DISPLAY=:99.0 -export PYTHONWARNINGS="all" -WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" - -tools/header.py "Run all tests with minimum dependencies" -nosetests --exe -v skimage - -tools/header.py "Pep8 and Flake tests" -flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples - -tools/header.py "Install optional dependencies" - -# http://unix.stackexchange.com/a/82615 -function retry() -{ - local n=0 - local try=$3 - local cmd="${@: 1}" - [[ $# -le 1 ]] && { - echo "Usage $0 "; } - - until [[ $n -ge $try ]] - do - $cmd && break || { - echo "Command Fail.." - ((n++)) - echo "retry $n ::" - sleep 1; - } - - done -} - -# Install Qt and then update the Matplotlib settings -if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then - retry sudo apt-get install -q python-qt4 - MPL_QT_API=PyQt4 - MPL_DIR=$HOME/.matplotlib - export QT_API=pyqt - -else - retry sudo apt-get install -q libqt4-dev - retry pip install PySide $WHEELHOUSE - retry python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - MPL_QT_API=PySide - MPL_DIR=$HOME/.config/matplotlib - export QT_API=pyside -fi - -# imread does NOT support py3.2 -if [[ $TRAVIS_PYTHON_VERSION != 3.2 ]]; then - retry sudo apt-get install -q libtiff4-dev libwebp-dev libpng12-dev xcftools - retry pip install imread -fi - -# TODO: update when SimpleITK become available on py34 or hopefully pip -if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then - retry easy_install SimpleITK -fi - -retry sudo apt-get install libfreeimage3 -retry pip install astropy - -if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then - retry pip install pyamg -fi - # Matplotlib settings - do not show figures during doc examples mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc From 479594653777c4ca9373cb25ddecd059e9e8ad7c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 17:20:23 -0500 Subject: [PATCH 09/25] Make new script executable --- tools/travis_install_optional.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/travis_install_optional.sh diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh old mode 100644 new mode 100755 From 0d2799a98dc5af0e776d32256b9926f47bd3eadb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 18:35:35 -0500 Subject: [PATCH 10/25] Export the MPL variables --- tools/travis_install_optional.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index c8d55fdf..0aa24a42 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -6,16 +6,16 @@ tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - MPL_QT_API=PyQt4 - MPL_DIR=$HOME/.matplotlib + export MPL_QT_API=PyQt4 + export MPL_DIR=$HOME/.matplotlib export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - MPL_QT_API=PySide - MPL_DIR=$HOME/.config/matplotlib + export MPL_QT_API=PySide + export MPL_DIR=$HOME/.config/matplotlib export QT_API=pyside fi From 37d6ddfe363ab5946d0f7304aed45bfdb0316d3e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 12 Oct 2014 19:11:32 -0500 Subject: [PATCH 11/25] Fix handling of variables - use local --- tools/travis_install_optional.sh | 6 ------ tools/travis_test.sh | 11 +++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index 0aa24a42..a373cc6a 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -6,17 +6,11 @@ tools/header.py "Install optional dependencies" # Install Qt and then update the Matplotlib settings if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then sudo apt-get install -q python-qt4 - export MPL_QT_API=PyQt4 - export MPL_DIR=$HOME/.matplotlib - export QT_API=pyqt else sudo apt-get install -q libqt4-dev pip install PySide $WHEELHOUSE python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install - export MPL_QT_API=PySide - export MPL_DIR=$HOME/.config/matplotlib - export QT_API=pyside fi # imread does NOT support py3.2 diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 3d35a9b1..4c2db692 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -1,6 +1,17 @@ #!/usr/bin/env bash set -ex +if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then + MPL_QT_API=PyQt4 + MPL_DIR=$HOME/.matplotlib + export QT_API=pyqt + +else + MPL_QT_API=PySide + MPL_DIR=$HOME/.config/matplotlib + export QT_API=pyside +fi + # Matplotlib settings - do not show figures during doc examples mkdir -p $MPL_DIR touch $MPL_DIR/matplotlibrc From 268eb5f2b54e8eebb0ba45063e660fd6b1092a1f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:57:05 -0500 Subject: [PATCH 12/25] Add tifffile to a new external package. --- skimage/external/tifffile/__init__.py | 4 + skimage/external/tifffile/_tifffile.py | 4839 ++++++++++++++++++++++++ skimage/io/_plugins/pil_plugin.py | 2 +- skimage/io/_plugins/tifffile_plugin.py | 7 +- 4 files changed, 4845 insertions(+), 7 deletions(-) create mode 100644 skimage/external/tifffile/__init__.py create mode 100644 skimage/external/tifffile/_tifffile.py 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 From dddbc1aa23ac626d9593f1e6ad60cecc59045efb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:57:56 -0500 Subject: [PATCH 13/25] Add the __init__.py to make it a package --- skimage/external/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 skimage/external/__init__.py diff --git a/skimage/external/__init__.py b/skimage/external/__init__.py new file mode 100644 index 00000000..e69de29b From fbba5e5e015d1d9d03380745686f01acb409d8c8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 05:58:54 -0500 Subject: [PATCH 14/25] Remove tifffile as an external dependency --- DEPENDS.txt | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index e5c10521..aa3cd021 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -16,7 +16,6 @@ Runtime requirements * `NetworkX `__ * `Pillow `__ (or `PIL `__) -* `Tifffile `__ Known build errors ------------------ diff --git a/requirements.txt b/requirements.txt index 7ce05c95..98f3b77f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,3 @@ scipy>=0.9 six>=1.3 networkx>=1.8 pillow>=1.1.7 -tifffile>=0.2 From b6f775a7cf8c775d89e8fe203cd2e0f874be5965 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 06:03:11 -0500 Subject: [PATCH 15/25] Use new wheels site and do not install tifffile until optional --- .travis.yml | 2 +- tools/travis_install_optional.sh | 2 ++ tools/travis_setup.sh | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a0e2b94..fb99c279 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ python: before_install: - export DISPLAY=:99.0 - export PYTHONWARNINGS="all" - - export WHEELHOUSE="--no-index --find-links=http://wheels.scikit-image.org/" + - export WHEELHOUSE="--no-index --find-links=http://travis-wheels.scikit-image.org/" - travis_retry tools/travis_setup.sh install: diff --git a/tools/travis_install_optional.sh b/tools/travis_install_optional.sh index a373cc6a..9ecb9d20 100755 --- a/tools/travis_install_optional.sh +++ b/tools/travis_install_optional.sh @@ -30,3 +30,5 @@ pip install astropy if [[ $TRAVIS_PYTHON_VERSION == 2.* ]]; then pip install pyamg fi + +pip install tifffile diff --git a/tools/travis_setup.sh b/tools/travis_setup.sh index cefe3a73..f3caaa5e 100755 --- a/tools/travis_setup.sh +++ b/tools/travis_setup.sh @@ -15,8 +15,6 @@ if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz fi -pip install $WHEELHOUSE numpy -pip install tifffile pip install -r requirements.txt $WHEELHOUSE python check_bento_build.py From 50aff0058a0b4801f0ec83ce442279fb211ea97d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 20:59:20 -0500 Subject: [PATCH 16/25] Add tifffile.c to the build --- bento.info | 3 + skimage/external/setup.py | 26 + skimage/external/tifffile/tifffile.c | 962 ++++++++++++++++++ .../tifffile/{_tifffile.py => tiffile_.py} | 2 + skimage/setup.py | 1 + 5 files changed, 994 insertions(+) create mode 100644 skimage/external/setup.py create mode 100644 skimage/external/tifffile/tifffile.c rename skimage/external/tifffile/{_tifffile.py => tiffile_.py} (99%) diff --git a/bento.info b/bento.info index 564f1f5a..7518abbc 100644 --- a/bento.info +++ b/bento.info @@ -157,6 +157,9 @@ Library: Extension: skimage.graph._ncut_cy Sources: skimage/graph/_ncut_cy.pyx + Extension: skimage.external.tifffile._tifffile + Sources: + skimage/external/tifffile/_tifffile.c Executable: skivi Module: skimage.scripts.skivi diff --git a/skimage/external/setup.py b/skimage/external/setup.py new file mode 100644 index 00000000..c046cd62 --- /dev/null +++ b/skimage/external/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import os.path + +base_path = os.path.abspath(os.path.dirname(__file__)) + + +def configuration(parent_package='', top_path=None): + from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs + + config = Configuration('external', parent_package, top_path) + + config.add_extension('tifffile._tifffile', + sources=['tifffile/_tifffile.c'], + include_dirs=[get_numpy_include_dirs()]) + return config + +if __name__ == '__main__': + from numpy.distutils.core import setup + setup(maintainer='scikit-image Developers', + maintainer_email='scikit-image@googlegroups.com', + description='External Libaries', + url='https://github.com/scikit-image/scikit-image', + license='Modified BSD', + **(configuration(top_path='').todict()) + ) diff --git a/skimage/external/tifffile/tifffile.c b/skimage/external/tifffile/tifffile.c new file mode 100644 index 00000000..6aa0eaa1 --- /dev/null +++ b/skimage/external/tifffile/tifffile.c @@ -0,0 +1,962 @@ + + +/* tifffile.c + +A Python C extension module for decoding PackBits and LZW encoded TIFF data. + +Refer to the tifffile.py module for documentation and tests. + +:Author: + `Christoph Gohlke `_ + +:Organization: + Laboratory for Fluorescence Dynamics, University of California, Irvine + +:Version: 2013.11.05 + +Install +------- +Use this Python distutils setup script to build the extension module:: + + # setup.py + # Usage: ``python setup.py build_ext --inplace`` + from distutils.core import setup, Extension + import numpy + setup(name='_tifffile', + ext_modules=[Extension('_tifffile', ['tifffile.c'], + include_dirs=[numpy.get_include()])]) + +License +------- +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. +*/ + +#define _VERSION_ "2013.11.05" + +#define WIN32_LEAN_AND_MEAN +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + +#include "Python.h" +#include "string.h" +#include "numpy/arrayobject.h" + +/* little endian by default */ +#ifndef MSB +#define MSB 1 +#endif + +#if MSB +#define LSB 0 +#define BOC '<' +#else +#define LSB 1 +#define BOC '>' +#endif + +#define NO_ERROR 0 +#define VALUE_ERROR -1 + +#ifdef _MSC_VER +typedef unsigned __int8 uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#ifdef _WIN64 +typedef __int64 ssize_t; +typedef signed __int64 intptr_t; +typedef unsigned __int64 uintptr_t; +#define SSIZE_MAX (9223372036854775808) +#else +typedef int ssize_t; +typedef _W64 signed int intptr_t; +typedef _W64 unsigned int uintptr_t; +#define SSIZE_MAX (2147483648) +#endif +#else +/* non MS compilers */ +#include +#include +#endif + +#define SWAP2BYTES(x) \ + ((((x) >> 8) & 0x00FF) | (((x) & 0x00FF) << 8)) + +#define SWAP4BYTES(x) \ + ((((x) >> 24) & 0x00FF) | (((x)&0x00FF) << 24) | \ + (((x) >> 8 ) & 0xFF00) | (((x)&0xFF00) << 8)) + +#define SWAP8BYTES(x) \ + ((((x) >> 56) & 0x00000000000000FF) | (((x) >> 40) & 0x000000000000FF00) | \ + (((x) >> 24) & 0x0000000000FF0000) | (((x) >> 8) & 0x00000000FF000000) | \ + (((x) << 8) & 0x000000FF00000000) | (((x) << 24) & 0x0000FF0000000000) | \ + (((x) << 40) & 0x00FF000000000000) | (((x) << 56) & 0xFF00000000000000)) + +struct BYTE_STRING { + unsigned int ref; /* reference count */ + unsigned int len; /* length of string */ + char *str; /* pointer to bytes */ +}; + +typedef union { + uint8_t b[2]; + uint16_t i; +} u_uint16; + +typedef union { + uint8_t b[4]; + uint32_t i; +} u_uint32; + +typedef union { + uint8_t b[8]; + uint64_t i; +} u_uint64; + +/*****************************************************************************/ +/* C functions */ + +/* Return mask for itemsize bits */ +unsigned char bitmask(const int itemsize) { + unsigned char result = 0; + unsigned char power = 1; + int i; + for (i = 0; i < itemsize; i++) { + result += power; + power *= 2; + } + return result << (8 - itemsize); +} + +/** Unpack sequence of tigthly packed 1-32 bit integers. + +Native byte order will be returned. + +Input data array should be padded to the next 16, 32 or 64-bit boundary +if itemsize not in (1, 2, 4, 8, 16, 24, 32, 64). + +*/ +int unpackbits( + unsigned char *data, + const ssize_t size, /** size of data in bytes */ + const int itemsize, /** number of bits in integer */ + ssize_t numitems, /** number of items to unpack */ + unsigned char *result /** buffer to store unpacked items */ + ) +{ + ssize_t i, j, k, storagesize; + unsigned char value; + /* Input validation is done in wrapper function */ + storagesize = (ssize_t)(ceil(itemsize / 8.0)); + storagesize = storagesize < 3 ? storagesize : storagesize > 4 ? 8 : 4; + switch (itemsize) { + case 8: + case 16: + case 32: + case 64: + memcpy(result, data, numitems*storagesize); + return NO_ERROR; + case 1: + for (i = 0, j = 0; i < numitems/8; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(128)) >> 7; + result[j++] = (value & (unsigned char)(64)) >> 6; + result[j++] = (value & (unsigned char)(32)) >> 5; + result[j++] = (value & (unsigned char)(16)) >> 4; + result[j++] = (value & (unsigned char)(8)) >> 3; + result[j++] = (value & (unsigned char)(4)) >> 2; + result[j++] = (value & (unsigned char)(2)) >> 1; + result[j++] = (value & (unsigned char)(1)); + } + if (numitems % 8) { + value = data[i]; + switch (numitems % 8) { + case 7: result[j+6] = (value & (unsigned char)(2)) >> 1; + case 6: result[j+5] = (value & (unsigned char)(4)) >> 2; + case 5: result[j+4] = (value & (unsigned char)(8)) >> 3; + case 4: result[j+3] = (value & (unsigned char)(16)) >> 4; + case 3: result[j+2] = (value & (unsigned char)(32)) >> 5; + case 2: result[j+1] = (value & (unsigned char)(64)) >> 6; + case 1: result[j] = (value & (unsigned char)(128)) >> 7; + } + } + return NO_ERROR; + case 2: + for (i = 0, j = 0; i < numitems/4; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(192)) >> 6; + result[j++] = (value & (unsigned char)(48)) >> 4; + result[j++] = (value & (unsigned char)(12)) >> 2; + result[j++] = (value & (unsigned char)(3)); + } + if (numitems % 4) { + value = data[i]; + switch (numitems % 4) { + case 3: result[j+2] = (value & (unsigned char)(12)) >> 2; + case 2: result[j+1] = (value & (unsigned char)(48)) >> 4; + case 1: result[j] = (value & (unsigned char)(192)) >> 6; + } + } + return NO_ERROR; + case 4: + for (i = 0, j = 0; i < numitems/2; i++) { + value = data[i]; + result[j++] = (value & (unsigned char)(240)) >> 4; + result[j++] = (value & (unsigned char)(15)); + } + if (numitems % 2) { + value = data[i]; + result[j] = (value & (unsigned char)(240)) >> 4; + } + return NO_ERROR; + case 24: + j = k = 0; + for (i = 0; i < numitems; i++) { + result[j++] = 0; + result[j++] = data[k++]; + result[j++] = data[k++]; + result[j++] = data[k++]; + } + return NO_ERROR; + } + /* 3, 5, 6, 7 */ + if (itemsize < 8) { + int shr = 16; + u_uint16 value, mask, tmp; + j = k = 0; + value.b[MSB] = data[j++]; + value.b[LSB] = data[j++]; + mask.b[MSB] = bitmask(itemsize); + mask.b[LSB] = 0; + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[LSB]; + if (shr < itemsize) { + value.b[MSB] = value.b[LSB]; + value.b[LSB] = data[j++]; + mask.i <<= 8 - itemsize; + shr += 8; + } else { + mask.i >>= itemsize; + } + } + return NO_ERROR; + } + /* 9, 10, 11, 12, 13, 14, 15 */ + if (itemsize < 16) { + int shr = 32; + u_uint32 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 3; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[3] = 0xFF; + mask.b[2] = bitmask(itemsize-8); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + if (shr < itemsize) { + value.b[3] = value.b[1]; + value.b[2] = value.b[0]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 16 - itemsize; + shr += 16; + } else { + mask.i >>= itemsize; + } + } +#else + /* not implemented */ +#endif + return NO_ERROR; + } + /* 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31 */ + if (itemsize < 32) { + int shr = 64; + u_uint64 value, mask, tmp; + mask.i = 0; + j = k = 0; +#if MSB + for (i = 7; i >= 0; i--) { + value.b[i] = data[j++]; + } + mask.b[7] = 0xFF; + mask.b[6] = 0xFF; + mask.b[5] = itemsize > 23 ? 0xFF : bitmask(itemsize-16); + mask.b[4] = itemsize < 24 ? 0x00 : bitmask(itemsize-24); + for (i = 0; i < numitems; i++) { + shr -= itemsize; + tmp.i = (value.i & mask.i) >> shr; + result[k++] = tmp.b[0]; /* swap bytes */ + result[k++] = tmp.b[1]; + result[k++] = tmp.b[2]; + result[k++] = tmp.b[3]; + if (shr < itemsize) { + value.b[7] = value.b[3]; + value.b[6] = value.b[2]; + value.b[5] = value.b[1]; + value.b[4] = value.b[0]; + value.b[3] = data[j++]; + value.b[2] = data[j++]; + value.b[1] = data[j++]; + value.b[0] = data[j++]; + mask.i <<= 32 - itemsize; + shr += 32; + } else { + mask.i >>= itemsize; + } + } +#else + /* Not implemented */ +#endif + return NO_ERROR; + } + return VALUE_ERROR; +} + +/*****************************************************************************/ +/* Python functions */ + +/** Unpack tightly packed integers. */ +char py_unpackints_doc[] = "Unpack groups of bits into numpy array."; + +static PyObject* +py_unpackints(PyObject *obj, PyObject *args, PyObject *kwds) +{ + PyObject *byteobj = NULL; + PyArrayObject *result = NULL; + PyArray_Descr *dtype = NULL; + char *encoded = NULL; + char *decoded = NULL; + Py_ssize_t encoded_len = 0; + Py_ssize_t decoded_len = 0; + Py_ssize_t runlen = 0; + Py_ssize_t i; + int storagesize, bytesize; + int itemsize = 0; + int skipbits = 0; + static char *kwlist[] = {"data", "dtype", "itemsize", "runlen", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&i|i", kwlist, + &byteobj, PyArray_DescrConverter, &dtype, &itemsize, &runlen)) + return NULL; + + Py_INCREF(byteobj); + + if (((itemsize < 1) || (itemsize > 32)) && (itemsize != 64)) { + PyErr_Format(PyExc_ValueError, "itemsize out of range"); + goto _fail; + } + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = PyBytes_GET_SIZE(byteobj); + bytesize = (int)ceil(itemsize / 8.0); + storagesize = bytesize < 3 ? bytesize : bytesize > 4 ? 8 : 4; + if ((encoded_len < bytesize) || (encoded_len > SSIZE_MAX / storagesize)) { + PyErr_Format(PyExc_ValueError, "data size out of range"); + goto _fail; + } + if (dtype->elsize != storagesize) { + PyErr_Format(PyExc_TypeError, "dtype.elsize doesn't fit itemsize"); + goto _fail; + } + + if (runlen == 0) { + runlen = (Py_ssize_t)(((uint64_t)encoded_len*8) / (uint64_t)itemsize); + } + skipbits = (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize) % 8); + if (skipbits > 0) { + skipbits = 8 - skipbits; + } + decoded_len = (Py_ssize_t)((uint64_t)runlen * (((uint64_t)encoded_len*8) / + ((uint64_t)runlen*(uint64_t)itemsize + (uint64_t)skipbits))); + + result = (PyArrayObject *)PyArray_SimpleNew(1, &decoded_len, + dtype->type_num); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "unable to allocate output array"); + goto _fail; + } + decoded = (char *)PyArray_DATA(result); + + for (i = 0; i < decoded_len; i+=runlen) { + if (NO_ERROR != + unpackbits((unsigned char *) encoded, + (ssize_t) encoded_len, + (int) itemsize, + (ssize_t) runlen, + (unsigned char *) decoded)) { + PyErr_Format(PyExc_ValueError, "unpackbits() failed"); + goto _fail; + } + encoded += (Py_ssize_t)(((uint64_t)runlen * (uint64_t)itemsize + + (uint64_t)skipbits) / 8); + decoded += runlen * storagesize; + } + + if ((dtype->byteorder != BOC) && (itemsize % 8 == 0)) { + switch (dtype->elsize) { + case 2: { + uint16_t *d = (uint16_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP2BYTES(*d); d++; + } + break; } + case 4: { + uint32_t *d = (uint32_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP4BYTES(*d); d++; + } + break; } + case 8: { + uint64_t *d = (uint64_t *)PyArray_DATA(result); + for (i = 0; i < PyArray_SIZE(result); i++) { + *d = SWAP8BYTES(*d); d++; + } + break; } + } + } + Py_DECREF(byteobj); + Py_DECREF(dtype); + return PyArray_Return(result); + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + Py_XDECREF(dtype); + return NULL; +} + + +/** Decode TIFF PackBits encoded string. */ +char py_decodepackbits_doc[] = "Return TIFF PackBits decoded string."; + +static PyObject * +py_decodepackbits(PyObject *obj, PyObject *args) +{ + int n; + char e; + char *decoded = NULL; + char *encoded = NULL; + char *encoded_end = NULL; + char *encoded_pos = NULL; + unsigned int encoded_len; + unsigned int decoded_len; + PyObject *byteobj = NULL; + PyObject *result = NULL; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + + /* release GIL: byte/string objects are immutable */ + Py_BEGIN_ALLOW_THREADS + + /* determine size of decoded string */ + encoded_pos = encoded; + encoded_end = encoded + encoded_len; + decoded_len = 0; + while (encoded_pos < encoded_end) { + n = (int)*encoded_pos++; + if (n >= 0) { + n++; + if (encoded_pos+n > encoded_end) + n = (int)(encoded_end - encoded_pos); + encoded_pos += n; + decoded_len += n; + } else if (n > -128) { + encoded_pos++; + decoded_len += 1-n; + } + } + Py_END_ALLOW_THREADS + + result = PyBytes_FromStringAndSize(0, decoded_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + decoded = PyBytes_AS_STRING(result); + + Py_BEGIN_ALLOW_THREADS + + /* decode string */ + encoded_end = encoded + encoded_len; + while (encoded < encoded_end) { + n = (int)*encoded++; + if (n >= 0) { + n++; + if (encoded+n > encoded_end) + n = (int)(encoded_end - encoded); + /* memmove(decoded, encoded, n); decoded += n; encoded += n; */ + while (n--) + *decoded++ = *encoded++; + } else if (n > -128) { + n = 1 - n; + e = *encoded++; + /* memset(decoded, e, n); decoded += n; */ + while (n--) + *decoded++ = e; + } + } + Py_END_ALLOW_THREADS + + Py_DECREF(byteobj); + return result; + + _fail: + Py_XDECREF(byteobj); + Py_XDECREF(result); + return NULL; +} + + +/** Decode TIFF LZW encoded string. */ +char py_decodelzw_doc[] = "Return TIFF LZW decoded string."; + +static PyObject * +py_decodelzw(PyObject *obj, PyObject *args) +{ + PyThreadState *_save = NULL; + PyObject *byteobj = NULL; + PyObject *result = NULL; + int i, j; + unsigned int encoded_len = 0; + unsigned int decoded_len = 0; + unsigned int result_len = 0; + unsigned int table_len = 0; + unsigned int len; + unsigned int code, c, oldcode, mask, shr; + uint64_t bitcount, bitw; + char *encoded = NULL; + char *result_ptr = NULL; + char *table2 = NULL; + char *cptr; + struct BYTE_STRING *decoded = NULL; + struct BYTE_STRING *decoded_ptr = NULL; + struct BYTE_STRING *table[4096]; + struct BYTE_STRING *newentry, *newresult, *t; + int little_endian = 0; + + if (!PyArg_ParseTuple(args, "O", &byteobj)) + return NULL; + + if (!PyBytes_Check(byteobj)) { + PyErr_Format(PyExc_TypeError, "expected byte string as input"); + goto _fail; + } + + Py_INCREF(byteobj); + encoded = PyBytes_AS_STRING(byteobj); + encoded_len = (unsigned int)PyBytes_GET_SIZE(byteobj); + /* + if (encoded_len >= 512 * 1024 * 1024) { + PyErr_Format(PyExc_ValueError, "encoded data > 512 MB not supported"); + goto _fail; + } + */ + /* release GIL: byte/string objects are immutable */ + _save = PyEval_SaveThread(); + + if ((*encoded != -128) || ((*(encoded+1) & 128))) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_ValueError, + "strip must begin with CLEAR code"); + goto _fail; + } + little_endian = (*(unsigned short *)encoded) & 128; + + /* allocate buffer for codes and pointers */ + decoded_len = 0; + len = (encoded_len + encoded_len/9) * sizeof(decoded); + decoded = PyMem_Malloc(len * sizeof(void *)); + if (decoded == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded"); + goto _fail; + } + memset((void *)decoded, 0, len * sizeof(void *)); + decoded_ptr = decoded; + + /* cache strings of length 2 */ + cptr = table2 = PyMem_Malloc(256*256*2 * sizeof(char)); + if (table2 == NULL) { + PyEval_RestoreThread(_save); + PyErr_Format(PyExc_MemoryError, "failed to allocate table2"); + goto _fail; + } + for (i = 0; i < 256; i++) { + for (j = 0; j < 256; j++) { + *cptr++ = (char)i; + *cptr++ = (char)j; + } + } + + memset(table, 0, sizeof(table)); + table_len = 258; + bitw = 9; + shr = 23; + mask = 4286578688; + bitcount = 0; + result_len = 0; + code = 0; + oldcode = 0; + + while ((unsigned int)((bitcount + bitw) / 8) <= encoded_len) { + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= (unsigned int)(bitcount % 8); + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + if (code == 256) { /* clearcode */ + /* initialize table and switch to 9 bit */ + while (table_len > 258) { + t = table[--table_len]; + t->ref--; + if (t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + bitw = 9; + shr = 23; + mask = 4286578688; + + /* read next code */ + code = *((unsigned int *)((void *)(encoded + (bitcount / 8)))); + if (little_endian) + code = SWAP4BYTES(code); + code <<= bitcount % 8; + code &= mask; + code >>= shr; + bitcount += bitw; + + if (code == 257) /* end of information */ + break; + + /* decoded.append(table[code]) */ + if (code < 256) { + result_len++; + *((int *)decoded_ptr++) = code; + } else { + newresult = table[code]; + newresult->ref++; + result_len += newresult->len; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + } + } else { + if (code < table_len) { + /* code is in table */ + /* newresult = table[code]; */ + /* newentry = table[oldcode] + table[code][0] */ + /* decoded.append(newresult); table.append(newentry) */ + if (code < 256) { + c = code; + *((unsigned int *)decoded_ptr++) = code; + result_len++; + } else { + newresult = table[code]; + newresult->ref++; + c = (unsigned int) *newresult->str; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + newentry = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry->ref = 1; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + (oldcode << 9) + + ((unsigned char)c << 1); + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = c; + } + table[table_len++] = newentry; + } else { + /* code is not in table */ + /* newentry = newresult = table[oldcode] + table[oldcode][0] */ + /* decoded.append(newresult); table.append(newentry) */ + newresult = PyMem_Malloc(sizeof(struct BYTE_STRING)); + newentry = newresult; + newentry->ref = 2; + if (oldcode < 256) { + newentry->len = 2; + newentry->str = table2 + 514*oldcode; + } else { + len = table[oldcode]->len; + newentry->len = len + 1; + newentry->str = PyMem_Malloc(newentry->len); + if (newentry->str == NULL) + break; + memmove(newentry->str, table[oldcode]->str, len); + newentry->str[len] = *table[oldcode]->str; + } + table[table_len++] = newentry; + *(struct BYTE_STRING **)decoded_ptr++ = newresult; + result_len += newresult->len; + } + } + oldcode = code; + /* increase bit-width if necessary */ + switch (table_len) { + case 511: + bitw = 10; + shr = 22; + mask = 4290772992; + break; + case 1023: + bitw = 11; + shr = 21; + mask = 4292870144; + break; + case 2047: + bitw = 12; + shr = 20; + mask = 4293918720; + } + } + + PyEval_RestoreThread(_save); + + if (code != 257) { + PyErr_WarnEx(NULL, + "py_decodelzw encountered unexpected end of stream", 1); + } + + /* result = ''.join(decoded) */ + decoded_len = (unsigned int)(decoded_ptr - decoded); + decoded_ptr = decoded; + result = PyBytes_FromStringAndSize(0, result_len); + if (result == NULL) { + PyErr_Format(PyExc_MemoryError, "failed to allocate decoded string"); + goto _fail; + } + result_ptr = PyBytes_AS_STRING(result); + + _save = PyEval_SaveThread(); + + while (decoded_len--) { + code = *((unsigned int *)decoded_ptr); + if (code < 256) { + *result_ptr++ = (char)code; + } else { + t = *((struct BYTE_STRING **)decoded_ptr); + memmove(result_ptr, t->str, t->len); + result_ptr += t->len; + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + decoded_ptr++; + } + PyMem_Free(decoded); + + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + PyMem_Free(table2); + + PyEval_RestoreThread(_save); + + Py_DECREF(byteobj); + return result; + + _fail: + if (table2 != NULL) + PyMem_Free(table2); + if (decoded != NULL) { + /* Bug? are decoded_ptr and decoded_len correct? */ + while (decoded_len--) { + code = *((unsigned int *) decoded_ptr); + if (code > 258) { + t = *((struct BYTE_STRING **) decoded_ptr); + if (--t->ref == 0) { + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + } + } + PyMem_Free(decoded); + } + while (table_len-- > 258) { + t = table[table_len]; + if (t->len > 2) + PyMem_Free(t->str); + PyMem_Free(t); + } + + Py_XDECREF(byteobj); + Py_XDECREF(result); + + return NULL; +} + +/*****************************************************************************/ +/* Create Python module */ + +char module_doc[] = + "A Python C extension module for decoding PackBits and LZW encoded " + "TIFF data.\n\n" + "Refer to the tifffile.py module for documentation and tests.\n\n" + "Authors:\n Christoph Gohlke \n" + " Laboratory for Fluorescence Dynamics, University of California, Irvine." + "\n\nVersion: %s\n"; + +static PyMethodDef module_methods[] = { +#if MSB + {"unpackints", (PyCFunction)py_unpackints, METH_VARARGS|METH_KEYWORDS, + py_unpackints_doc}, +#endif + {"decodelzw", (PyCFunction)py_decodelzw, METH_VARARGS, + py_decodelzw_doc}, + {"decodepackbits", (PyCFunction)py_decodepackbits, METH_VARARGS, + py_decodepackbits_doc}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION >= 3 + +struct module_state { + PyObject *error; +}; + +#define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) + +static int module_traverse(PyObject *m, visitproc visit, void *arg) { + Py_VISIT(GETSTATE(m)->error); + return 0; +} + +static int module_clear(PyObject *m) { + Py_CLEAR(GETSTATE(m)->error); + return 0; +} + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_tifffile", + NULL, + sizeof(struct module_state), + module_methods, + NULL, + module_traverse, + module_clear, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC +PyInit__tifffile(void) + +#else + +#define INITERROR return + +PyMODINIT_FUNC +init_tifffile(void) + +#endif +{ + PyObject *module; + + char *doc = (char *)PyMem_Malloc(sizeof(module_doc) + sizeof(_VERSION_)); + PyOS_snprintf(doc, sizeof(doc), module_doc, _VERSION_); + +#if PY_MAJOR_VERSION >= 3 + moduledef.m_doc = doc; + module = PyModule_Create(&moduledef); +#else + module = Py_InitModule3("_tifffile", module_methods, doc); +#endif + + PyMem_Free(doc); + + if (module == NULL) + INITERROR; + + if (_import_array() < 0) { + Py_DECREF(module); + INITERROR; + } + + { +#if PY_MAJOR_VERSION < 3 + PyObject *s = PyString_FromString(_VERSION_); +#else + PyObject *s = PyUnicode_FromString(_VERSION_); +#endif + PyObject *dict = PyModule_GetDict(module); + PyDict_SetItemString(dict, "__version__", s); + Py_DECREF(s); + } + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} + diff --git a/skimage/external/tifffile/_tifffile.py b/skimage/external/tifffile/tiffile_.py similarity index 99% rename from skimage/external/tifffile/_tifffile.py rename to skimage/external/tifffile/tiffile_.py index 83ae4f0e..b2d3303a 100644 --- a/skimage/external/tifffile/_tifffile.py +++ b/skimage/external/tifffile/tiffile_.py @@ -151,6 +151,8 @@ from xml.etree import cElementTree as etree import numpy +from . import _tifffile + __version__ = '0.3.1' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', diff --git a/skimage/setup.py b/skimage/setup.py index 962adb97..9a8c8ac2 100644 --- a/skimage/setup.py +++ b/skimage/setup.py @@ -21,6 +21,7 @@ def configuration(parent_package='', top_path=None): config.add_subpackage('transform') config.add_subpackage('util') config.add_subpackage('segmentation') + config.add_subpackage('external') def add_test_directories(arg, dirname, fnames): if dirname.split(os.path.sep)[-1] == 'tests': From 88719b33402986137d070d9dd3dac2bc86de4932 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 21:00:52 -0500 Subject: [PATCH 17/25] Fix path to tifffile.c --- skimage/external/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/external/setup.py b/skimage/external/setup.py index c046cd62..343c4d54 100644 --- a/skimage/external/setup.py +++ b/skimage/external/setup.py @@ -11,7 +11,7 @@ def configuration(parent_package='', top_path=None): config = Configuration('external', parent_package, top_path) config.add_extension('tifffile._tifffile', - sources=['tifffile/_tifffile.c'], + sources=['tifffile/tifffile.c'], include_dirs=[get_numpy_include_dirs()]) return config From a3c8ce32ac9d3a142f3aa42d532510e130811998 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 15 Oct 2014 21:05:23 -0500 Subject: [PATCH 18/25] Rename file to a valid module name. --- skimage/external/tifffile/__init__.py | 2 +- skimage/external/tifffile/{tiffile_.py => tifffile_local.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename skimage/external/tifffile/{tiffile_.py => tifffile_local.py} (100%) diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py index da90abdf..9ca9ec84 100644 --- a/skimage/external/tifffile/__init__.py +++ b/skimage/external/tifffile/__init__.py @@ -1,4 +1,4 @@ try: from tifffile import * except ImportError: - from ._tifffile import * + from .tifffile_local import * diff --git a/skimage/external/tifffile/tiffile_.py b/skimage/external/tifffile/tifffile_local.py similarity index 100% rename from skimage/external/tifffile/tiffile_.py rename to skimage/external/tifffile/tifffile_local.py From 284cdd0bcac4c10098d68466b4d28655bc525226 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 03:20:09 -0500 Subject: [PATCH 19/25] Do not test or cover the external libraries directly --- .coveragerc | 2 +- tools/travis_test.sh | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 992eafdc..ffab8979 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,7 @@ source = skimage include = */skimage/* omit = */setup.py - */skimage/io/_plugins/tifffile.py + */skimage/external/* [report] exclude_lines = diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 4c2db692..78d406a7 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,10 +40,9 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage +TEST_ARGS='--exe -v --with-doctest --ignore-files="skimage/external/*"' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then - export TEST_ARGS="--with-cov --cover-package skimage" -else - export TEST_ARGS="" + TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi -nosetests --exe -v --with-doctest $TEST_ARGS +nosetests $TEST_ARGS From 0569524d7929da0d1ada6cc815ec14b46bd806a0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 04:14:42 -0500 Subject: [PATCH 20/25] Use proper nose option --- tools/travis_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 78d406a7..2b8588de 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,7 +40,7 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage -TEST_ARGS='--exe -v --with-doctest --ignore-files="skimage/external/*"' +TEST_ARGS='--exe -v --with-doctest --exclude="skimage/external/*"' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi From b5da90e4c7126037b47e452460d04939af967a10 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 04:49:45 -0500 Subject: [PATCH 21/25] Just disable doctests in tifffile_local --- skimage/external/tifffile/tifffile_local.py | 72 ++++++++++----------- tools/travis_test.sh | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index b2d3303a..17d430b0 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -116,13 +116,13 @@ References Examples -------- ->>> data = numpy.random.rand(5, 301, 219) ->>> imsave('temp.tif', data) +>> data = numpy.random.rand(5, 301, 219) +>> imsave('temp.tif', data) ->>> image = imread('temp.tif') ->>> numpy.testing.assert_array_equal(image, data) +>> image = imread('temp.tif') +>> numpy.testing.assert_array_equal(image, data) ->>> with TiffFile('temp.tif') as tif: +>> with TiffFile('temp.tif') as tif: ... images = tif.asarray() ... for page in tif: ... for tag in page.tags.values(): @@ -180,9 +180,9 @@ def imsave(filename, data, **kwargs): Examples -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) - >>> imsave('temp.tif', data, compress=6, + >> 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)]) """ @@ -209,8 +209,8 @@ class TiffWriter(object): Examples -------- - >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> with TiffWriter('temp.tif', bigtiff=True) as tif: + >> 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) @@ -663,11 +663,11 @@ def imread(files, **kwargs): Examples -------- - >>> im = imread('test.tif', key=0) - >>> im.shape + >> im = imread('test.tif', key=0) + >> im.shape (256, 256, 4) - >>> ims = imread(['test.tif', 'test.tif']) - >>> ims.shape + >> ims = imread(['test.tif', 'test.tif']) + >> ims.shape (2, 256, 256, 4) """ @@ -733,7 +733,7 @@ class TiffFile(object): Examples -------- - >>> with TiffFile('test.tif') as tif: + >> with TiffFile('test.tif') as tif: ... data = tif.asarray() ... data.shape (256, 256, 4) @@ -2215,11 +2215,11 @@ class TiffSequence(object): Examples -------- - >>> tifs = TiffSequence("test.oif.files/*.tif") - >>> tifs.shape, tifs.axes + >> tifs = TiffSequence("test.oif.files/*.tif") + >> tifs.shape, tifs.axes ((2, 100), 'CT') - >>> data = tifs.asarray() - >>> data.shape + >> data = tifs.asarray() + >> data.shape (2, 100, 256, 256) """ @@ -3316,12 +3316,12 @@ def unpackrgb(data, dtype='>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >>> print(unpackrgb(data, '> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + >> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') ((5, 2, 1), 'TYX') """ @@ -3397,7 +3397,7 @@ def transpose_axes(data, axes, asaxes='CTZYX'): A view is returned if possible. - >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + >> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape (5, 2, 1, 3, 4) """ @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >>> stripnull(b'string\\x00') + >> stripnull(b'string\\x00') b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >>> stripascii(b'string\\x00string\\n\\x01\\x00') + >> stripascii(b'string\\x00string\\n\\x01\\x00') b'string\\x00string\\n' - >>> stripascii(b'\\x00') + >> stripascii(b'\\x00') b'' """ @@ -3490,9 +3490,9 @@ def format_size(size): def sequence(value): """Return tuple containing value if value is not a sequence. - >>> sequence(1) + >> sequence(1) (1,) - >>> sequence([1]) + >> sequence([1]) [1] """ @@ -3508,9 +3508,9 @@ def product(iterable): Equivalent of functools.reduce(operator.mul, iterable, 1). - >>> product([2**8, 2**30]) + >> product([2**8, 2**30]) 274877906944 - >>> product([]) + >> product([]) 1 """ @@ -3525,7 +3525,7 @@ def natural_sorted(iterable): E.g. for sorting file names. - >>> natural_sorted(['f1', 'f2', 'f10']) + >> natural_sorted(['f1', 'f2', 'f10']) ['f1', 'f2', 'f10'] """ @@ -3540,7 +3540,7 @@ def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): Convert LSM time stamps. - >>> excel_datetime(40237.029999999795) + >> excel_datetime(40237.029999999795) datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) """ @@ -3552,7 +3552,7 @@ def julian_datetime(julianday, milisecond=0): Convert Julian dates according to MetaMorph. - >>> julian_datetime(2451576, 54362783) + >> julian_datetime(2451576, 54362783) datetime.datetime(2000, 2, 2, 15, 6, 2, 783) """ @@ -3586,7 +3586,7 @@ def test_tifffile(directory='testimages', verbose=True): Print error message on failure. - >>> test_tifffile(verbose=False) + >> test_tifffile(verbose=False) """ successful = 0 diff --git a/tools/travis_test.sh b/tools/travis_test.sh index 2b8588de..dddd6d07 100755 --- a/tools/travis_test.sh +++ b/tools/travis_test.sh @@ -40,7 +40,7 @@ echo 'backend.qt4 : '$MPL_QT_API >> $MPL_DIR/matplotlibrc tools/header.py "Run tests with all dependencies" # run tests again with optional dependencies to get more coverage -TEST_ARGS='--exe -v --with-doctest --exclude="skimage/external/*"' +TEST_ARGS='--exe -v --with-doctest' if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then TEST_ARGS="$TEST_ARGS --with-cov --cover-package skimage" fi From 17a3b50a5ed1cc77d438f003ea32d1f4e77d8ca9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 05:05:37 -0500 Subject: [PATCH 22/25] Use tifffile 0.3.2, which passes doctests --- skimage/external/tifffile/tifffile_local.py | 80 ++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index 17d430b0..29b4bbdb 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -116,13 +116,13 @@ References Examples -------- ->> data = numpy.random.rand(5, 301, 219) ->> imsave('temp.tif', data) +>>> data = numpy.random.rand(5, 301, 219) +>>> imsave('temp.tif', data) ->> image = imread('temp.tif') ->> numpy.testing.assert_array_equal(image, data) +>>> image = imread('temp.tif') +>>> numpy.testing.assert_array_equal(image, data) ->> with TiffFile('temp.tif') as tif: +>>> with TiffFile('temp.tif') as tif: ... images = tif.asarray() ... for page in tif: ... for tag in page.tags.values(): @@ -153,7 +153,7 @@ import numpy from . import _tifffile -__version__ = '0.3.1' +__version__ = '0.3.2' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', 'TiffSequence') @@ -180,9 +180,9 @@ def imsave(filename, data, **kwargs): Examples -------- - >> data = numpy.random.rand(2, 5, 3, 301, 219) - >> description = u'{"shape": %s}' % str(list(data.shape)) - >> imsave('temp.tif', data, compress=6, + >>> 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)]) """ @@ -209,8 +209,8 @@ class TiffWriter(object): Examples -------- - >> data = numpy.random.rand(2, 5, 3, 301, 219) - >> with TiffWriter('temp.tif', bigtiff=True) as tif: + >>> 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) @@ -663,12 +663,12 @@ def imread(files, **kwargs): Examples -------- - >> im = imread('test.tif', key=0) - >> im.shape - (256, 256, 4) - >> ims = imread(['test.tif', 'test.tif']) - >> ims.shape - (2, 256, 256, 4) + >>> im = imread('temp.tif', key=0) + >>> im.shape + (3, 301, 219) + >>> ims = imread(['temp.tif', 'temp.tif']) + >>> ims.shape + (2, 10, 3, 301, 219) """ kwargs_file = {} @@ -733,10 +733,10 @@ class TiffFile(object): Examples -------- - >> with TiffFile('test.tif') as tif: + >>> with TiffFile('temp.tif') as tif: ... data = tif.asarray() ... data.shape - (256, 256, 4) + (5, 301, 219) """ def __init__(self, arg, name=None, offset=None, size=None, @@ -2215,11 +2215,11 @@ class TiffSequence(object): Examples -------- - >> tifs = TiffSequence("test.oif.files/*.tif") - >> tifs.shape, tifs.axes + >>> tifs = TiffSequence("test.oif.files/*.tif") # doctest: +SKIP + >>> tifs.shape, tifs.axes # doctest: +SKIP ((2, 100), 'CT') - >> data = tifs.asarray() - >> data.shape + >>> data = tifs.asarray() # doctest: +SKIP + >>> data.shape # doctest: +SKIP (2, 100, 256, 256) """ @@ -3316,12 +3316,12 @@ def unpackrgb(data, dtype='> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) - >> print(unpackrgb(data, '>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff) + >>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> print(unpackrgb(data, '>> print(unpackrgb(data, '> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') + >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC') ((5, 2, 1), 'TYX') """ @@ -3397,7 +3397,7 @@ def transpose_axes(data, axes, asaxes='CTZYX'): A view is returned if possible. - >> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape + >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape (5, 2, 1, 3, 4) """ @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >> stripnull(b'string\\x00') + >>> stripnull(b'string\\x00') b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >> stripascii(b'string\\x00string\\n\\x01\\x00') + >>> stripascii(b'string\\x00string\\n\\x01\\x00') b'string\\x00string\\n' - >> stripascii(b'\\x00') + >>> stripascii(b'\\x00') b'' """ @@ -3490,9 +3490,9 @@ def format_size(size): def sequence(value): """Return tuple containing value if value is not a sequence. - >> sequence(1) + >>> sequence(1) (1,) - >> sequence([1]) + >>> sequence([1]) [1] """ @@ -3508,9 +3508,9 @@ def product(iterable): Equivalent of functools.reduce(operator.mul, iterable, 1). - >> product([2**8, 2**30]) + >>> product([2**8, 2**30]) 274877906944 - >> product([]) + >>> product([]) 1 """ @@ -3525,7 +3525,7 @@ def natural_sorted(iterable): E.g. for sorting file names. - >> natural_sorted(['f1', 'f2', 'f10']) + >>> natural_sorted(['f1', 'f2', 'f10']) ['f1', 'f2', 'f10'] """ @@ -3540,7 +3540,7 @@ def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)): Convert LSM time stamps. - >> excel_datetime(40237.029999999795) + >>> excel_datetime(40237.029999999795) datetime.datetime(2010, 2, 28, 0, 43, 11, 999982) """ @@ -3552,7 +3552,7 @@ def julian_datetime(julianday, milisecond=0): Convert Julian dates according to MetaMorph. - >> julian_datetime(2451576, 54362783) + >>> julian_datetime(2451576, 54362783) datetime.datetime(2000, 2, 2, 15, 6, 2, 783) """ @@ -3586,7 +3586,7 @@ def test_tifffile(directory='testimages', verbose=True): Print error message on failure. - >> test_tifffile(verbose=False) + >>> test_tifffile(verbose=False) """ successful = 0 From 713452ab9eacbcff32dbd1a075aa872883344f40 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 05:28:43 -0500 Subject: [PATCH 23/25] Use newer version of tifffile.py. --- skimage/external/tifffile/tifffile_local.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/external/tifffile/tifffile_local.py b/skimage/external/tifffile/tifffile_local.py index 29b4bbdb..b5a67ccf 100644 --- a/skimage/external/tifffile/tifffile_local.py +++ b/skimage/external/tifffile/tifffile_local.py @@ -153,7 +153,7 @@ import numpy from . import _tifffile -__version__ = '0.3.2' +__version__ = '0.3.3' __docformat__ = 'restructuredtext en' __all__ = ('imsave', 'imread', 'imshow', 'TiffFile', 'TiffWriter', 'TiffSequence') @@ -181,7 +181,7 @@ def imsave(filename, data, **kwargs): Examples -------- >>> data = numpy.random.rand(2, 5, 3, 301, 219) - >>> description = u'{"shape": %s}' % str(list(data.shape)) + >>> description = '{"shape": %s}' % str(list(data.shape)) >>> imsave('temp.tif', data, compress=6, ... extratags=[(270, 's', 0, description, True)]) @@ -3448,7 +3448,7 @@ def stripnull(string): Clean NULL terminated C strings. - >>> stripnull(b'string\\x00') + >>> stripnull(b'string\\x00') # doctest: +SKIP b'string' """ @@ -3461,9 +3461,9 @@ def stripascii(string): Clean NULL separated and terminated TIFF strings. - >>> stripascii(b'string\\x00string\\n\\x01\\x00') + >>> stripascii(b'string\\x00string\\n\\x01\\x00') # doctest: +SKIP b'string\\x00string\\n' - >>> stripascii(b'\\x00') + >>> stripascii(b'\\x00') # doctest: +SKIP b'' """ From 7734cdf7e02b26952b30445190c1f35dd703ba59 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 16 Oct 2014 17:06:12 -0500 Subject: [PATCH 24/25] Move tifffile license description --- skimage/{io => external}/LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename skimage/{io => external}/LICENSE.txt (91%) diff --git a/skimage/io/LICENSE.txt b/skimage/external/LICENSE.txt similarity index 91% rename from skimage/io/LICENSE.txt rename to skimage/external/LICENSE.txt index 16672bcb..d9f04072 100644 --- a/skimage/io/LICENSE.txt +++ b/skimage/external/LICENSE.txt @@ -1,5 +1,5 @@ -_plugins.tifffile.py +tifffile Copyright (c) 2008-2014, Christoph Gohlke Copyright (c) 2008-2014, The Regents of the University of California From 95f4aa70c1bf5e3e471fe7e91bbe091e8afbd41b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 20 Oct 2014 05:03:00 -0500 Subject: [PATCH 25/25] Avoid star import --- skimage/external/tifffile/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/external/tifffile/__init__.py b/skimage/external/tifffile/__init__.py index 9ca9ec84..bd426a3b 100644 --- a/skimage/external/tifffile/__init__.py +++ b/skimage/external/tifffile/__init__.py @@ -1,4 +1,4 @@ try: - from tifffile import * + from tifffile import imread, imsave, TiffFile except ImportError: - from .tifffile_local import * + from .tifffile_local import imread, imsave, TiffFile