diff --git a/skimage/data/no_time_for_that.gif b/skimage/data/no_time_for_that.gif new file mode 100644 index 00000000..45607b95 Binary files /dev/null and b/skimage/data/no_time_for_that.gif differ diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index e1390685..e8f302e8 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -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 ' 1: + return np.array(frames) + else: + return frames[0] def _palette_is_grayscale(pil_image): diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 83811fc0..b7df8a3a 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -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 diff --git a/skimage/io/tests/test_multi_image.py b/skimage/io/tests/test_multi_image.py index ebaa71dc..6542ab2e 100644 --- a/skimage/io/tests/test_multi_image.py +++ b/skimage/io/tests/test_multi_image.py @@ -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__":