Merge pull request #1200 from blink1073/multiimage_tifffile

Standardize handling of multi-image files
This commit is contained in:
Stefan van der Walt
2014-11-07 15:24:34 +02:00
4 changed files with 231 additions and 218 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

+56 -24
View File
@@ -9,7 +9,7 @@ from skimage.external.tifffile import (
imread as tif_imread, imsave as tif_imsave)
def imread(fname, dtype=None):
def imread(fname, dtype=None, img_num=None, **kwargs):
"""Load an image from file.
Parameters
@@ -18,6 +18,12 @@ def imread(fname, dtype=None):
File name.
dtype : numpy dtype object or string specifier
Specifies data type of array elements.
img_num : int, optional
Specifies which image to read in a file with multiple images
(zero-indexed).
kwargs : keyword pairs, optional
Addition keyword arguments to pass through (only applicable to Tiff
files for now, see `tifffile`'s `imread` function).
Notes
-----
@@ -34,8 +40,9 @@ def imread(fname, dtype=None):
"""
if hasattr(fname, 'lower') and dtype is None:
kwargs.setdefault('key', img_num)
if fname.lower().endswith(('.tiff', '.tif')):
return tif_imread(fname)
return tif_imread(fname, **kwargs)
im = Image.open(fname)
try:
@@ -45,10 +52,10 @@ def imread(fname, dtype=None):
site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries"
raise ValueError('Could not load "%s"\nPlease see documentation at: %s' % (fname, site))
else:
return pil_to_ndarray(im, dtype)
return pil_to_ndarray(im, dtype=dtype)
def pil_to_ndarray(im, dtype=None):
def pil_to_ndarray(im, dtype=None, img_num=None):
"""Import a PIL Image object to an ndarray, in memory.
Parameters
@@ -56,27 +63,52 @@ def pil_to_ndarray(im, dtype=None):
Refer to ``imread``.
"""
fp = im.fp if hasattr(im, 'fp') else None
if im.mode == 'P':
if _palette_is_grayscale(im):
im = im.convert('L')
frames = []
i = 0
while 1:
try:
im.seek(i)
except EOFError:
break
# seeking must be done sequentially
if img_num and not i == img_num:
i += 1
continue
frame = im
if im.mode == 'P':
if _palette_is_grayscale(im):
frame = im.convert('L')
else:
frame = im.convert('RGB')
elif im.mode == '1':
frame = im.convert('L')
elif 'A' in im.mode:
frame = im.convert('RGBA')
if 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')
frame = np.fromstring(frame.tostring(), dtype)
frame.shape = shape[::-1]
else:
im = im.convert('RGB')
elif im.mode == '1':
im = im.convert('L')
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:
im = im.convert('RGBA')
im = np.array(im, dtype=dtype)
if fp is not None:
fp.close()
return im
frame = np.array(frame, dtype=dtype)
frames.append(frame)
i += 1
if hasattr(im, 'fp') and im.fp:
im.fp.close()
if len(frames) > 1:
return np.array(frames)
else:
return frames[0]
def _palette_is_grayscale(pil_image):
+122 -158
View File
@@ -9,6 +9,9 @@ from copy import copy
import numpy as np
import six
from PIL import Image
from skimage.external.tifffile import TiffFile
__all__ = ['MultiImage', 'ImageCollection', 'concatenate_images',
@@ -71,156 +74,8 @@ def alphanumeric_key(s):
return k
class MultiImage(object):
"""A class containing a single multi-frame image.
Parameters
----------
filename : str
The complete path to the image file.
conserve_memory : bool, optional
Whether to conserve memory by only caching a single frame. Default is
True.
Notes
-----
If ``conserve_memory=True`` the memory footprint can be reduced, however
the performance can be affected because frames have to be read from file
more often.
The last accessed frame is cached, all other frames will have to be read
from file.
The current implementation makes use of PIL.
Examples
--------
>>> from skimage import data_dir
>>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP
>>> len(img) # doctest: +SKIP
2
>>> for frame in img: # doctest: +SKIP
... print(frame.shape) # doctest: +SKIP
(15, 10)
(15, 10)
"""
def __init__(self, filename, conserve_memory=True, dtype=None):
"""Load a multi-img."""
self._filename = filename
self._conserve_memory = conserve_memory
self._dtype = dtype
self._cached = None
from PIL import Image
img = Image.open(self._filename)
if self._conserve_memory:
self._numframes = self._find_numframes(img)
else:
self._frames = self._getallframes(img)
self._numframes = len(self._frames)
@property
def filename(self):
return self._filename
@property
def conserve_memory(self):
return self._conserve_memory
def _find_numframes(self, img):
"""Find the number of frames in the multi-img."""
i = 0
while True:
i += 1
try:
img.seek(i)
except EOFError:
break
return i
def _getframe(self, framenum):
"""Open the image and extract the frame."""
from PIL import Image
img = Image.open(self.filename)
img.seek(framenum)
return np.asarray(img, dtype=self._dtype)
def _getallframes(self, img):
"""Extract all frames from the multi-img."""
frames = []
try:
i = 0
while True:
frames.append(np.asarray(img, dtype=self._dtype))
i += 1
img.seek(i)
except EOFError:
return frames
def __getitem__(self, n):
"""Return the n-th frame as an array.
Parameters
----------
n : int
Number of the required frame.
Returns
-------
frame : ndarray
The n-th frame.
"""
numframes = self._numframes
if -numframes <= n < numframes:
n = n % numframes
else:
raise IndexError("There are only %s frames in the image"
% numframes)
if self.conserve_memory:
if not self._cached == n:
frame = self._getframe(n)
self._cached = n
self._cachedframe = frame
return self._cachedframe
else:
return self._frames[n]
def __iter__(self):
"""Iterate over the frames."""
for i in range(len(self)):
yield self[i]
def __len__(self):
"""Number of images in collection."""
return self._numframes
def __str__(self):
return str(self.filename) + ' [%s frames]' % self._numframes
def concatenate(self):
"""Concatenate all images in the multi-image into an array.
Returns
-------
ar : np.ndarray
An array having one more dimension than the images in `self`.
See Also
--------
concatenate_images
Raises
------
ValueError
If images in the `MultiImage` don't have identical shapes.
"""
return concatenate_images(self)
class ImageCollection(object):
"""Load and manage a collection of image files.
Note that files are always stored in alphabetical order. Also note that
@@ -279,6 +134,9 @@ class ImageCollection(object):
ic = ImageCollection('/tmp/*.png', load_func=imread_convert)
For files with multiple images, the images will be flattened into a list
and added to the list of available images.
Examples
--------
>>> import skimage.io as io
@@ -293,7 +151,9 @@ class ImageCollection(object):
>>> ic = io.ImageCollection('/tmp/work/*.png:/tmp/other/*.jpg')
"""
def __init__(self, load_pattern, conserve_memory=True, load_func=None):
def __init__(self, load_pattern, conserve_memory=True, load_func=None,
**load_func_kwargs):
"""Load and manage a collection of images."""
if isinstance(load_pattern, six.string_types):
load_pattern = load_pattern.split(os.pathsep)
@@ -301,13 +161,16 @@ class ImageCollection(object):
for pattern in load_pattern:
self._files.extend(glob(pattern))
self._files = sorted(self._files, key=alphanumeric_key)
self._numframes = self._find_images()
else:
self._files = load_pattern
self._numframes = len(load_pattern)
self._frame_index = None
if conserve_memory:
memory_slots = 1
else:
memory_slots = len(self._files)
memory_slots = self._numframes
self._conserve_memory = conserve_memory
self._cached = None
@@ -318,6 +181,8 @@ class ImageCollection(object):
else:
self.load_func = load_func
self.load_func_kwargs = load_func_kwargs
self.data = np.empty(memory_slots, dtype=object)
@property
@@ -328,6 +193,35 @@ class ImageCollection(object):
def conserve_memory(self):
return self._conserve_memory
def _find_images(self):
index = []
for fname in self._files:
if fname.lower().endswith(('.tiff', '.tif')):
img = TiffFile(fname)
index += [(fname, i) for i in range(len(img.pages))]
else:
im = Image.open(fname)
try:
# this will raise an IOError if the file is not readable
im.getdata()[0]
except IOError:
site = "http://pillow.readthedocs.org/en/latest/installation.html#external-libraries"
raise ValueError('Could not load "%s"\nPlease see documentation at: %s' % (fname, site))
else:
i = 0
while True:
i += 1
index.append((fname, i))
try:
im.seek(i)
except EOFError:
break
if hasattr(im, 'fp') and im.fp:
im.fp.close()
self._frame_index = index
return len(index)
def __getitem__(self, n):
"""Return selected image(s) in the collection.
@@ -356,9 +250,15 @@ class ImageCollection(object):
n = self._check_imgnum(n)
idx = n % len(self.data)
if (self.conserve_memory and n != self._cached) or \
(self.data[idx] is None):
self.data[idx] = self.load_func(self.files[n])
if ((self.conserve_memory and n != self._cached) or
(self.data[idx] is None)):
if self._frame_index:
fname, img_num = self._frame_index[idx]
self.data[idx] = self.load_func(fname, img_num=img_num,
**self.load_func_kwargs)
else:
self.data[idx] = self.load_func(self.files[n],
**self.load_func_kwargs)
self._cached = n
return self.data[idx]
@@ -367,9 +267,17 @@ class ImageCollection(object):
# object. Any loaded image data in the original ImageCollection
# will be copied by reference to the new object. Image data
# loaded after this creation is not linked.
fidx = range(len(self.files))[n]
fidx = range(self._numframes)[n]
new_ic = copy(self)
new_ic._files = [self.files[i] for i in fidx]
if self._frame_index:
new_ic._files = [self._frame_index[i][0] for i in fidx]
new_ic._frame_index = [self._frame_index[i] for i in fidx]
else:
new_ic._files = [self._files[i] for i in fidx]
new_ic._numframes = len(fidx)
if self.conserve_memory:
if self._cached in fidx:
new_ic._cached = fidx.index(self._cached)
@@ -382,7 +290,7 @@ class ImageCollection(object):
def _check_imgnum(self, n):
"""Check that the given image number is valid."""
num = len(self.files)
num = self._numframes
if -num <= n < num:
n = n % num
else:
@@ -397,7 +305,7 @@ class ImageCollection(object):
def __len__(self):
"""Number of images in collection."""
return len(self.files)
return self._numframes
def __str__(self):
return str(self.files)
@@ -458,3 +366,59 @@ def imread_collection_wrapper(imread):
return ImageCollection(load_pattern, conserve_memory=conserve_memory,
load_func=imread)
return imread_collection
class MultiImage(ImageCollection):
"""A class containing a single multi-frame image.
Parameters
----------
filename : str
The complete path to the image file.
conserve_memory : bool, optional
Whether to conserve memory by only caching a single frame. Default is
True.
Notes
-----
If ``conserve_memory=True`` the memory footprint can be reduced, however
the performance can be affected because frames have to be read from file
more often.
The last accessed frame is cached, all other frames will have to be read
from file.
The current implementation makes use of ``tifffile`` for Tiff files and
PIL otherwise.
Examples
--------
>>> from skimage import data_dir
>>> img = MultiImage(data_dir + '/multipage.tif') # doctest: +SKIP
>>> len(img) # doctest: +SKIP
2
>>> for frame in img: # doctest: +SKIP
... print(frame.shape) # doctest: +SKIP
(15, 10)
(15, 10)
"""
def __init__(self, filename, conserve_memory=True, dtype=None,
**imread_kwargs):
"""Load a multi-img."""
from ._io import imread
def load_func(fname, **kwargs):
kwargs.setdefault('dtype', dtype)
return imread(fname, **kwargs)
self._filename = filename
super(MultiImage, self).__init__(filename, conserve_memory,
load_func=load_func, **imread_kwargs)
@property
def filename(self):
return self._filename
+53 -36
View File
@@ -1,18 +1,10 @@
import os
import numpy as np
from numpy.testing.decorators import skipif
from numpy.testing import assert_raises, assert_equal, assert_allclose
from skimage import data_dir
from skimage.io.collection import MultiImage
try:
from PIL import Image
except ImportError:
PIL_available = False
else:
PIL_available = True
from skimage.io.collection import MultiImage, ImageCollection
import six
@@ -22,46 +14,71 @@ class TestMultiImage():
def setUp(self):
# This multipage TIF file was created with imagemagick:
# convert im1.tif im2.tif -adjoin multipage.tif
if PIL_available:
self.img = MultiImage(os.path.join(data_dir, 'multipage.tif'))
paths = [os.path.join(data_dir, 'multipage.tif'),
os.path.join(data_dir, 'no_time_for_that.gif')]
self.imgs = [MultiImage(paths[0]),
MultiImage(paths[0], conserve_memory=False),
MultiImage(paths[1]),
MultiImage(paths[1], conserve_memory=False),
ImageCollection(paths[0]),
ImageCollection(paths[1], conserve_memory=False),
ImageCollection('%s:%s' % (paths[0], paths[1]))]
@skipif(not PIL_available)
def test_len(self):
assert len(self.img) == 2
assert len(self.imgs[0]) == len(self.imgs[1]) == 2
assert len(self.imgs[2]) == len(self.imgs[3]) == 24
assert len(self.imgs[4]) == 2
assert len(self.imgs[5]) == 24
assert len(self.imgs[6]) == 26
def test_slicing(self):
img = self.imgs[-1]
assert type(img[:]) is ImageCollection
assert len(img[:]) == 26
assert len(img[:1]) == 1
assert len(img[1:]) == 25
assert_allclose(img[0], img[:1][0])
assert_allclose(img[1], img[1:][0])
assert_allclose(img[-1], img[::-1][0])
assert_allclose(img[0], img[::-1][-1])
@skipif(not PIL_available)
def test_getitem(self):
num = len(self.img)
for i in range(-num, num):
assert type(self.img[i]) is np.ndarray
assert_allclose(self.img[0], self.img[-num])
for img in self.imgs:
num = len(img)
# assert_raises expects a callable, hence this thin wrapper function.
def return_img(n):
return self.img[n]
assert_raises(IndexError, return_img, num)
assert_raises(IndexError, return_img, -num - 1)
for i in range(-num, num):
assert type(img[i]) is np.ndarray
assert_allclose(img[0], img[-num])
# assert_raises expects a callable, hence this thin wrapper function.
def return_img(n):
return img[n]
assert_raises(IndexError, return_img, num)
assert_raises(IndexError, return_img, -num - 1)
@skipif(not PIL_available)
def test_files_property(self):
assert isinstance(self.img.filename, six.string_types)
for img in self.imgs:
if isinstance(img, ImageCollection):
continue
def set_filename(f):
self.img.filename = f
assert_raises(AttributeError, set_filename, 'newfile')
assert isinstance(img.filename, six.string_types)
def set_filename(f):
img.filename = f
assert_raises(AttributeError, set_filename, 'newfile')
@skipif(not PIL_available)
def test_conserve_memory_property(self):
assert isinstance(self.img.conserve_memory, bool)
for img in self.imgs:
assert isinstance(img.conserve_memory, bool)
def set_mem(val):
self.img.conserve_memory = val
assert_raises(AttributeError, set_mem, True)
def set_mem(val):
img.conserve_memory = val
assert_raises(AttributeError, set_mem, True)
@skipif(not PIL_available)
def test_concatenate(self):
array = self.img.concatenate()
assert_equal(array.shape, (len(self.img),) + self.img[0].shape)
for img in self.imgs:
array = img.concatenate()
assert_equal(array.shape, (len(img),) + img[0].shape)
if __name__ == "__main__":