diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 140842ab..30f432f4 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -182,3 +182,6 @@ - Axel Donath Blob Detection + +- Adam Feuer + PIL Image import and export improvements diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index e6f9c491..eb6467df 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -19,9 +19,28 @@ from six import string_types def imread(fname, dtype=None): """Load an image from file. + Parameters + ---------- + fname : str + File name. + dtype : numpy dtype object or string specifier + Specifies data type of array elements. + + """ im = Image.open(fname) - fp = im.fp + return pil_to_ndarray(im, dtype) + + +def pil_to_ndarray(im, dtype=None): + """Import a PIL Image object to an ndarray, in memory. + + Parameters + ---------- + 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') @@ -37,7 +56,8 @@ def imread(fname, dtype=None): elif 'A' in im.mode: im = im.convert('RGBA') im = np.array(im, dtype=dtype) - fp.close() + if fp is not None: + fp.close() return im @@ -65,24 +85,12 @@ def _palette_is_grayscale(pil_image): return np.allclose(np.diff(valid_palette), 0) -def imsave(fname, arr, format_str=None): - """Save an image to disk. +def ndarray_to_pil(arr, format_str=None): + """Export an ndarray to a PIL object. Parameters ---------- - fname : str or file-like object - Name of destination file. - arr : ndarray of uint8 or float - Array (image) to save. Arrays of data-type uint8 should have - values in [0, 255], whereas floating-point arrays must be - in [0, 1]. - format_str: str - Format to save as, this is defaulted to PNG if using a file-like - object; this will be derived from the extension if fname is a string - - Notes - ----- - Currently, only 8-bit precision is supported. + Refer to ``imsave``. """ arr = np.asarray(arr).squeeze() @@ -109,10 +117,6 @@ def imsave(fname, arr, format_str=None): # Force all integers to bytes arr = arr.astype(np.uint8) - # default to PNG if file-like object - if not isinstance(fname, string_types) and format_str is None: - format_str = "PNG" - try: img = Image.frombytes(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) @@ -120,6 +124,34 @@ def imsave(fname, arr, format_str=None): img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) + return img + + +def imsave(fname, arr, format_str=None): + """Save an image to disk. + + Parameters + ---------- + fname : str or file-like object + Name of destination file. + arr : ndarray of uint8 or float + Array (image) to save. Arrays of data-type uint8 should have + values in [0, 255], whereas floating-point arrays must be + in [0, 1]. + format_str: str + Format to save as, this is defaulted to PNG if using a file-like + object; this will be derived from the extension if fname is a string + + Notes + ----- + Currently, only 8-bit precision is supported. + + """ + # default to PNG if file-like object + if not isinstance(fname, string_types) and format_str is None: + format_str = "PNG" + + img = ndarray_to_pil(arr, format_str=None) img.save(fname, format=format_str) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index c5718388..5cd36f92 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -14,7 +14,7 @@ from six import BytesIO try: from PIL import Image - from skimage.io._plugins.pil_plugin import _palette_is_grayscale + from skimage.io._plugins.pil_plugin import pil_to_ndarray, ndarray_to_pil, _palette_is_grayscale use_plugin('pil') except ImportError: PIL_available = False @@ -113,26 +113,40 @@ def test_imread_uint16_big_endian(): class TestSave: - def roundtrip(self, dtype, x, scaling=1): + def roundtrip_file(self, x): f = NamedTemporaryFile(suffix='.png') fname = f.name f.close() imsave(fname, x) y = imread(fname) + return y + def roundtrip_pil_image(self, x): + pil_image = ndarray_to_pil(x) + y = pil_to_ndarray(pil_image) + return y + + def verify_roundtrip(self, dtype, x, y, scaling=1): assert_array_almost_equal((x * scaling).astype(np.int32), y) - @skipif(not PIL_available) - def test_imsave_roundtrip(self): + def verify_imsave_roundtrip(self, roundtrip_function): 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) if np.issubdtype(dtype, float): - yield self.roundtrip, dtype, x, 255 + yield self.verify_roundtrip, dtype, x, roundtrip_function(x), 255 else: x = (x * 255).astype(dtype) - yield self.roundtrip, dtype, 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) + + @skipif(not PIL_available) + def test_imsave_roundtrip_pil_image(self): + self.verify_imsave_roundtrip(self.roundtrip_pil_image) @skipif(not PIL_available) @@ -151,5 +165,14 @@ def test_imsave_filelike(): assert_allclose(out, image) +@skipif(not PIL_available) +def test_imexport_imimport(): + shape = (2, 2) + image = np.zeros(shape) + pil_image = ndarray_to_pil(image) + out = pil_to_ndarray(pil_image) + assert out.shape == shape + + if __name__ == "__main__": run_module_suite()