Merge pull request #1189 from blink1073/fix-pil-plugin

Fix PIL handling of non-uint8 images and add `tifffile` support
This commit is contained in:
Stefan van der Walt
2014-10-11 11:49:34 +02:00
16 changed files with 6086 additions and 101 deletions
+1
View File
@@ -6,6 +6,7 @@ source = skimage
include = */skimage/*
omit =
*/setup.py
*/skimage/io/_plugins/tifffile.py
[report]
exclude_lines =
+2 -1
View File
@@ -23,6 +23,7 @@ before_install:
- sudo apt-get update
- travis_retry pip install wheel flake8 coveralls nose
- travis_retry pip uninstall -y numpy
# on Python 2.7, use the system versions of numpy, scipy, and matplotlib
# and the minimum version of cython and networkx
@@ -32,7 +33,7 @@ before_install:
pip install https://github.com/networkx/networkx/archive/networkx-1.8.tar.gz;
fi
- travis_retry pip install -r requirements.txt $WHEELHOUSE;
- travis_retry pip install -r requirements.txt $WHEELHOUSE;
- python check_bento_build.py
install:
+3
View File
@@ -157,6 +157,9 @@ 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
+1 -1
View File
@@ -27,4 +27,4 @@
failing (useful for installing from third party sources)
- Feel free to cancel a build rather than waiting for it to go to completion
if you have made a change to that branch.
- A VM with 64bit Ubuntu 12.04 is a huge help.
- A VM with 64bit Ubuntu 12.04 is a huge help for debugging.
+97
View File
@@ -1,7 +1,14 @@
"""Testing utilities."""
import os
import re
from tempfile import NamedTemporaryFile
from skimage import (
data, io, img_as_uint, img_as_float, img_as_int, img_as_ubyte)
from numpy import testing
import numpy as np
SKIP_RE = re.compile("(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$")
@@ -75,3 +82,93 @@ def doctest_skip_parser(func):
new_lines.append(code)
func.__doc__ = "\n".join(new_lines)
return func
def roundtrip(img, plugin, suffix):
"""Save and read an image using a specified plugin"""
if not '.' in suffix:
suffix = '.' + suffix
temp_file = NamedTemporaryFile(suffix=suffix, delete=False)
temp_file.close()
fname = temp_file.name
io.imsave(fname, img, plugin=plugin)
new = io.imread(fname, plugin=plugin)
try:
os.remove(fname)
except Exception:
pass
return new
def color_check(plugin, fmt='png'):
"""Check roundtrip behavior for color images.
All major input types should be handled as ubytes and read
back correctly.
"""
img = img_as_ubyte(data.chelsea())
r1 = roundtrip(img, plugin, fmt)
testing.assert_allclose(img, r1)
img2 = img > 128
r2 = roundtrip(img2, plugin, fmt)
testing.assert_allclose(img2.astype(np.uint8), r2)
img3 = img_as_float(img)
r3 = roundtrip(img3, plugin, fmt)
testing.assert_allclose(r3, img)
img4 = img_as_int(img)
if fmt.lower() in (('tif', 'tiff')):
img4 -= 100
r4 = roundtrip(img4, plugin, fmt)
testing.assert_allclose(r4, img4)
else:
r4 = roundtrip(img4, plugin, fmt)
testing.assert_allclose(r4, img_as_ubyte(img4))
img5 = img_as_uint(img)
r5 = roundtrip(img5, plugin, fmt)
testing.assert_allclose(r5, img)
def mono_check(plugin, fmt='png'):
"""Check the roundtrip behavior for images that support most types.
All major input types should be handled.
"""
img = img_as_ubyte(data.moon())
r1 = roundtrip(img, plugin, fmt)
testing.assert_allclose(img, r1)
img2 = img > 128
r2 = roundtrip(img2, plugin, fmt)
testing.assert_allclose(img2.astype(np.uint8), r2)
img3 = img_as_float(img)
r3 = roundtrip(img3, plugin, fmt)
if r3.dtype.kind == 'f':
testing.assert_allclose(img3, r3)
else:
testing.assert_allclose(r3, img_as_uint(img))
img4 = img_as_int(img)
if fmt.lower() in (('tif', 'tiff')):
img4 -= 100
r4 = roundtrip(img4, plugin, fmt)
testing.assert_allclose(r4, img4)
else:
r4 = roundtrip(img4, plugin, fmt)
testing.assert_allclose(r4, img_as_uint(img4))
img5 = img_as_uint(img)
r5 = roundtrip(img5, plugin, fmt)
testing.assert_allclose(r5, img5)
if __name__ == '__main__':
color_check('pil')
mono_check('pil')
mono_check('pil', 'bmp')
mono_check('pil', 'tiff')
+8
View File
@@ -0,0 +1,8 @@
_plugins.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
Distributed under the terms of the BSD License.
+94 -31
View File
@@ -1,4 +1,4 @@
__all__ = ['imread']
__all__ = ['imread', 'imsave']
import numpy as np
@@ -11,9 +11,10 @@ except ImportError:
"http://pypi.python.org/pypi/PIL/) "
"for further instructions.")
from skimage.util import img_as_ubyte
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
def imread(fname, dtype=None):
@@ -26,8 +27,24 @@ def imread(fname, dtype=None):
dtype : numpy dtype object or string specifier
Specifies data type of array elements.
Notes
-----
Tiff files are handled by Christophe Golhke's tifffile.py [1]_, and support many
advanced image types including multi-page and floating point.
All other files are read using the Python Imaging Libary.
See PIL docs [2]_ for a list of supported formats.
References
----------
.. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html
.. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
"""
if hasattr(fname, 'lower') and dtype is None:
if fname.lower().endswith(('.tiff', '.tif')):
return tif_imread(fname)
im = Image.open(fname)
try:
# this will raise an IOError if the file is not readable
@@ -58,6 +75,8 @@ def pil_to_ndarray(im, dtype=None):
elif im.mode.startswith('I;16'):
shape = im.size
dtype = '>u2' if im.mode.endswith('B') else '<u2'
if 'S' in im.mode:
dtype = dtype.replace('u', 'i')
im = np.fromstring(im.tostring(), dtype)
im.shape = shape[::-1]
elif 'A' in im.mode:
@@ -100,38 +119,41 @@ def ndarray_to_pil(arr, format_str=None):
Refer to ``imsave``.
"""
arr = np.asarray(arr).squeeze()
if arr.ndim not in (2, 3):
raise ValueError("Invalid shape for image array: %s" % arr.shape)
if arr.ndim == 3:
if arr.shape[2] not in (3, 4):
raise ValueError("Invalid number of channels in image array.")
# Image is floating point, assume in [0, 1]
if np.issubdtype(arr.dtype, float):
arr = arr * 255
arr = arr.astype(np.uint8)
if arr.ndim == 2:
mode = 'L'
elif arr.shape[2] in (3, 4):
arr = img_as_ubyte(arr)
mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]]
# Force all integers to bytes
arr = arr.astype(np.uint8)
elif format_str in ['png', 'PNG']:
mode = 'I;16'
mode_base = 'I'
try:
img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]),
arr.tostring())
except AttributeError:
img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]),
arr.tostring())
if arr.dtype.kind == 'f':
arr = img_as_uint(arr)
return img
elif arr.max() < 256 and arr.min() >= 0:
arr = arr.astype(np.uint8)
mode = mode_base = 'L'
else:
arr = img_as_uint(arr)
else:
arr = img_as_ubyte(arr)
mode = 'L'
mode_base = 'L'
if arr.ndim == 2:
im = Image.new(mode_base, arr.T.shape)
im.fromstring(arr.tostring(), 'raw', mode)
else:
try:
im = Image.frombytes(mode, (arr.shape[1], arr.shape[0]),
arr.tostring())
except AttributeError:
im = Image.fromstring(mode, (arr.shape[1], arr.shape[0]),
arr.tostring())
return im
def imsave(fname, arr, format_str=None):
@@ -151,14 +173,55 @@ def imsave(fname, arr, format_str=None):
Notes
-----
Currently, only 8-bit precision is supported.
Tiff files are handled by Christophe Golhke's tifffile.py [1]_,
and support many advanced image types including multi-page and
floating point.
All other image formats use the Python Imaging Libary.
See PIL docs [2]_ for a list of other supported formats.
All images besides single channel PNGs are converted using `img_as_uint8`.
Single Channel PNGs have the following behavior:
- Integer values in [0, 255] and Boolean types -> img_as_uint8
- Floating point and other integers -> img_as_uint16
References
----------
.. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html
.. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
"""
# default to PNG if file-like object
if not isinstance(fname, string_types) and format_str is None:
format_str = "PNG"
# Check for png in filename
if (isinstance(fname, string_types)
and fname.lower().endswith(".png")):
format_str = "PNG"
img = ndarray_to_pil(arr, format_str=None)
arr = np.asanyarray(arr).squeeze()
if arr.dtype.kind == 'b':
arr = arr.astype(np.uint8)
use_tif = False
if hasattr(fname, 'lower'):
if fname.lower().endswith(('.tiff', '.tif')):
use_tif = True
if not format_str is None:
if format_str.lower() in ['tiff', 'tif']:
use_tif = True
if use_tif:
tif_imsave(fname, arr)
return
if arr.ndim not in (2, 3):
raise ValueError("Invalid shape for image array: %s" % arr.shape)
if arr.ndim == 3:
if arr.shape[2] not in (3, 4):
raise ValueError("Invalid number of channels in image array.")
img = ndarray_to_pil(arr, format_str=format_str)
img.save(fname, format=format_str)
+962
View File
@@ -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 <http://www.lfd.uci.edu/~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 <stdint.h>
#include <limits.h>
#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 <http://www.lfd.uci.edu/~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
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 "
+1 -4
View File
@@ -44,10 +44,7 @@ plugin_meta_data = {}
# the following preferences.
preferred_plugins = {
# Default plugins for all types (overridden by specific types below).
'all': ['matplotlib', 'pil', 'qt', 'freeimage', 'null'],
# Use PIL as the default imread plugin, since matplotlib (1.2.x)
# is buggy (flips PNGs around, returns bytes as floats, etc.)
'imread': ['pil'],
'all': ['pil', 'matplotlib', 'qt', 'freeimage', 'null']
}
+4
View File
@@ -27,6 +27,10 @@ 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__':
-12
View File
@@ -29,17 +29,5 @@ def test_imread_url():
assert image.shape == (512, 512)
@raises(RuntimeError)
def test_imread_no_plugin():
# tweak data path so that file URI works on both unix and windows.
image_path = os.path.join(data_dir, 'lena.png')
plugins = plugin_store['imread']
plugin_store['imread'] = []
try:
io.imread(image_path)
finally:
plugin_store['imread'] = plugins
if __name__ == "__main__":
run_module_suite()
+48 -31
View File
@@ -1,25 +1,22 @@
import os.path
import numpy as np
from numpy.testing import *
from numpy.testing.decorators import skipif
from numpy.testing import (
assert_array_equal, assert_array_almost_equal, assert_raises,
assert_allclose, run_module_suite)
from tempfile import NamedTemporaryFile
from skimage import data_dir
from skimage.io import (imread, imsave, use_plugin, reset_plugins,
Image as ioImage)
from skimage._shared.testing import mono_check, color_check
from six import BytesIO
try:
from PIL import Image
from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale
use_plugin('pil')
except ImportError:
PIL_available = False
else:
PIL_available = True
from PIL import Image
from skimage.io._plugins.pil_plugin import (
pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale)
use_plugin('pil')
np.random.seed(0)
@@ -39,8 +36,6 @@ def setup_module(self):
pass
@skipif(not PIL_available)
def test_imread_flatten():
# a color image is flattened
img = imread(os.path.join(data_dir, 'color.png'), flatten=True)
@@ -51,7 +46,6 @@ def test_imread_flatten():
assert np.sctype2char(img.dtype) in np.typecodes['AllInteger']
@skipif(not PIL_available)
def test_imread_palette():
img = imread(os.path.join(data_dir, 'palette_gray.png'))
assert img.ndim == 2
@@ -59,7 +53,6 @@ def test_imread_palette():
assert img.ndim == 3
@skipif(not PIL_available)
def test_palette_is_gray():
gray = Image.open(os.path.join(data_dir, 'palette_gray.png'))
assert _palette_is_grayscale(gray)
@@ -67,7 +60,6 @@ def test_palette_is_gray():
assert not _palette_is_grayscale(color)
@skipif(not PIL_available)
def test_bilevel():
expected = np.zeros((10, 10))
expected[::2] = 255
@@ -76,7 +68,6 @@ def test_bilevel():
assert_array_equal(img, expected)
@skipif(not PIL_available)
def test_imread_uint16():
expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy'))
img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16.tif'))
@@ -84,7 +75,6 @@ def test_imread_uint16():
assert_array_almost_equal(img, expected)
@skipif(not PIL_available)
def test_repr_png():
img_path = os.path.join(data_dir, 'camera.png')
original_img = ioImage(imread(img_path))
@@ -97,18 +87,16 @@ def test_repr_png():
assert np.all(original_img == round_trip)
@skipif(not PIL_available)
def test_imread_truncated_jpg():
assert_raises((IOError, ValueError), imread,
os.path.join(data_dir, 'truncated.jpg'))
# Big endian images not correctly loaded for PIL < 1.1.7
# Renable test when PIL 1.1.7 is more common.
@skipif(True)
def test_imread_uint16_big_endian():
expected = np.load(os.path.join(data_dir, 'chessboard_GRAY_U8.npy'))
img = imread(os.path.join(data_dir, 'chessboard_GRAY_U16B.tif'))
assert img.dtype == np.dtype('>u2')
assert img.dtype == np.uint16
assert_array_almost_equal(img, expected)
@@ -135,21 +123,20 @@ class TestSave:
x = np.ones(shape, dtype=dtype) * np.random.rand(*shape)
if np.issubdtype(dtype, float):
yield self.verify_roundtrip, dtype, x, roundtrip_function(x), 255
yield (self.verify_roundtrip, dtype, x,
roundtrip_function(x), 255)
else:
x = (x * 255).astype(dtype)
yield self.verify_roundtrip, dtype, x, roundtrip_function(x)
yield (self.verify_roundtrip, dtype, x,
roundtrip_function(x))
@skipif(not PIL_available)
def test_imsave_roundtrip_file(self):
self.verify_imsave_roundtrip(self.roundtrip_file)
self.verify_imsave_roundtrip(self.roundtrip_file)
@skipif(not PIL_available)
def test_imsave_roundtrip_pil_image(self):
self.verify_imsave_roundtrip(self.roundtrip_pil_image)
self.verify_imsave_roundtrip(self.roundtrip_pil_image)
@skipif(not PIL_available)
def test_imsave_filelike():
shape = (2, 2)
image = np.zeros(shape)
@@ -165,7 +152,6 @@ def test_imsave_filelike():
assert_allclose(out, image)
@skipif(not PIL_available)
def test_imexport_imimport():
shape = (2, 2)
image = np.zeros(shape)
@@ -174,5 +160,36 @@ def test_imexport_imimport():
assert out.shape == shape
def test_all_color():
color_check('pil')
color_check('pil', 'bmp')
def test_all_mono():
mono_check('pil')
mono_check('pil', 'tiff')
class TestSaveTIF:
def roundtrip(self, dtype, x):
f = NamedTemporaryFile(suffix='.tif')
fname = f.name
f.close()
imsave(fname, x)
y = imread(fname)
assert_array_equal(x, y)
def test_imsave_roundtrip(self):
for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]:
for dtype in (np.uint8, np.uint16, np.int16, np.float32,
np.float64, np.bool):
x = np.random.rand(*shape)
if not np.issubdtype(dtype, float) and not dtype == np.bool:
x = (x * np.iinfo(dtype).max).astype(dtype)
else:
x = x.astype(dtype)
yield self.roundtrip, dtype, x
if __name__ == "__main__":
run_module_suite()
+12 -15
View File
@@ -3,17 +3,14 @@ import skimage as si
import skimage.io as sio
import numpy as np
from numpy.testing import *
from numpy.testing.decorators import skipif
from numpy.testing import (
assert_array_equal, assert_array_almost_equal, run_module_suite)
from tempfile import NamedTemporaryFile
try:
import skimage.io._plugins.tifffile_plugin as tf
_plugins = sio.plugin_order()
TF_available = True
sio.use_plugin('tifffile')
except ImportError:
TF_available = False
_plugins = sio.plugin_order()
sio.use_plugin('tifffile')
np.random.seed(0)
@@ -22,7 +19,6 @@ def teardown():
sio.reset_plugins()
@skipif(not TF_available)
def test_imread_uint16():
expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy'))
img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16.tif'))
@@ -30,7 +26,6 @@ def test_imread_uint16():
assert_array_almost_equal(img, expected)
@skipif(not TF_available)
def test_imread_uint16_big_endian():
expected = np.load(os.path.join(si.data_dir, 'chessboard_GRAY_U8.npy'))
img = sio.imread(os.path.join(si.data_dir, 'chessboard_GRAY_U16B.tif'))
@@ -47,14 +42,16 @@ class TestSave:
y = sio.imread(fname)
assert_array_equal(x, y)
@skipif(not TF_available)
def test_imsave_roundtrip(self):
for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]:
for dtype in (np.uint8, np.uint16, np.float32, np.float64):
x = np.ones(shape, dtype=dtype) * np.random.rand(*shape)
for dtype in (np.uint8, np.uint16, np.float32, np.int16,
np.float64):
x = np.random.rand(*shape)
if not np.issubdtype(dtype, float):
x = (x * 255).astype(dtype)
x = (x * np.iinfo(dtype).max).astype(dtype)
else:
x = x.astype(dtype)
yield self.roundtrip, dtype, x
+5 -5
View File
@@ -1,6 +1,6 @@
import os
from skimage import data, img_as_float, io
from skimage import data, img_as_float, io, img_as_uint
from skimage.viewer import ImageViewer, viewer_available
from skimage.viewer.widgets import (
Slider, OKCancelButtons, SaveButtons, ComboBox, CheckBox, Text)
@@ -27,10 +27,10 @@ def test_check_box():
cb.val = False
assert_equal(cb.val, False)
cb.val = 1
assert_equal(cb.val, True)
assert_equal(cb.val, True)
cb.val = 0
assert_equal(cb.val, False)
@skipif(not viewer_available)
def test_combo_box():
@@ -101,8 +101,8 @@ def test_save_buttons():
sv.save_to_stack()
sv.save_to_file(filename)
img = img_as_float(data.imread(filename))
assert_almost_equal(img, viewer.image)
img = data.imread(filename)
assert_almost_equal(img, img_as_uint(viewer.image))
img = io.pop()
assert_almost_equal(img, viewer.image)