mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-18 12:40:14 +08:00
Merge pull request #1200 from blink1073/multiimage_tifffile
Standardize handling of multi-image files
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 887 KiB |
@@ -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
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user