From d8709bd0c0a0f332dbee9284d4a4ae0183c9bc62 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 8 Nov 2009 23:49:51 +0200 Subject: [PATCH] io: Add imsave using PIL. --- scikits/image/io/_plugins/pil_plugin.ini | 2 +- scikits/image/io/_plugins/pil_plugin.py | 116 ++++++++++++------ .../io/tests/{test_imread.py => test_pil.py} | 31 ++++- 3 files changed, 109 insertions(+), 40 deletions(-) rename scikits/image/io/tests/{test_imread.py => test_pil.py} (50%) diff --git a/scikits/image/io/_plugins/pil_plugin.ini b/scikits/image/io/_plugins/pil_plugin.ini index be54e541..7afa244f 100644 --- a/scikits/image/io/_plugins/pil_plugin.ini +++ b/scikits/image/io/_plugins/pil_plugin.ini @@ -1,4 +1,4 @@ [pil] description = Image reading via the Python Imaging Library -provides = imread +provides = imread, imsave diff --git a/scikits/image/io/_plugins/pil_plugin.py b/scikits/image/io/_plugins/pil_plugin.py index e50cbe6c..2aa3e9d4 100644 --- a/scikits/image/io/_plugins/pil_plugin.py +++ b/scikits/image/io/_plugins/pil_plugin.py @@ -6,44 +6,90 @@ import numpy as np try: from PIL import Image except ImportError: - print 'Could not load Python Imaging Library' -else: - def imread(fname, as_grey=False, dtype=None): - """Load an image from file. + raise ImportError("The Python Image Library could not be found. " + "Please refer to http://pypi.python.org/pypi/PIL/ " + "for further instructions.") - """ - im = Image.open(fname) - if im.mode == 'P': - if palette_is_grayscale(im): - im = im.convert('L') - else: - im = im.convert('RGB') +def imread(fname, as_grey=False, dtype=None): + """Load an image from file. - if as_grey and not \ - im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'): - im = im.convert('F') + """ + im = Image.open(fname) + if im.mode == 'P': + if _palette_is_grayscale(im): + im = im.convert('L') + else: + im = im.convert('RGB') - return np.array(im, dtype=dtype) + if as_grey and not \ + im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'): + im = im.convert('F') - def palette_is_grayscale(pil_image): - """Return True if PIL image in palette mode is grayscale. + return np.array(im, dtype=dtype) - Parameters - ---------- - pil_image : PIL image - PIL Image that is in Palette mode. +def _palette_is_grayscale(pil_image): + """Return True if PIL image in palette mode is grayscale. - Returns - ------- - is_grayscale : bool - True if all colors in image palette are gray. - """ - assert pil_image.mode == 'P' - # get palette as an array with R, G, B columns - palette = np.asarray(pil_image.getpalette()).reshape((256, 3)) - # Not all palette colors are used; unused colors have junk values. - start, stop = pil_image.getextrema() - valid_palette = palette[start:stop] - # Image is grayscale if channel differences (R - G and G - B) - # are all zero. - return np.allclose(np.diff(valid_palette), 0) + Parameters + ---------- + pil_image : PIL image + PIL Image that is in Palette mode. + + Returns + ------- + is_grayscale : bool + True if all colors in image palette are gray. + """ + assert pil_image.mode == 'P' + # get palette as an array with R, G, B columns + palette = np.asarray(pil_image.getpalette()).reshape((256, 3)) + # Not all palette colors are used; unused colors have junk values. + start, stop = pil_image.getextrema() + valid_palette = palette[start:stop] + # Image is grayscale if channel differences (R - G and G - B) + # are all zero. + return np.allclose(np.diff(valid_palette), 0) + +def imsave(fname, arr): + """Save an image to disk. + + Parameters + ---------- + fname : str + 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]. + + Notes + ----- + Currently, only 8-bit precision is supported. + + """ + 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): + mode = {3: 'RGB', 4: 'RGBA'}[arr.shape[2]] + + # Force all integers to bytes + arr = arr.astype(np.uint8) + + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) + img.save(fname) diff --git a/scikits/image/io/tests/test_imread.py b/scikits/image/io/tests/test_pil.py similarity index 50% rename from scikits/image/io/tests/test_imread.py rename to scikits/image/io/tests/test_pil.py index fc77e0c7..0dbb877f 100644 --- a/scikits/image/io/tests/test_imread.py +++ b/scikits/image/io/tests/test_pil.py @@ -1,9 +1,12 @@ import os.path import numpy as np +from numpy.testing import * + +from tempfile import NamedTemporaryFile from scikits.image import data_dir -from scikits.image.io import imread -from scikits.image.io._plugins.pil_plugin import palette_is_grayscale +from scikits.image.io import imread, imsave +from scikits.image.io._plugins.pil_plugin import _palette_is_grayscale def test_imread_flatten(): # a color image is flattened and returned as float32 @@ -26,6 +29,26 @@ def test_imread_palette(): def test_palette_is_gray(): from PIL import Image gray = Image.open(os.path.join(data_dir, 'palette_gray.png')) - assert palette_is_grayscale(gray) + assert _palette_is_grayscale(gray) color = Image.open(os.path.join(data_dir, 'palette_color.png')) - assert not palette_is_grayscale(color) + assert not _palette_is_grayscale(color) + +class TestSave: + def roundtrip(self, dtype, x, scaling=1): + f = NamedTemporaryFile(suffix='.png') + imsave(f.name, x) + f.seek(0) + y = imread(f.name) + + assert_array_almost_equal((x * scaling).astype(np.int32), 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.float32, np.float64): + x = np.ones(shape, dtype=dtype) * np.random.random(shape) + + if np.issubdtype(dtype, float): + yield self.roundtrip, dtype, x, 255 + else: + x = (x * 255).astype(dtype) + yield self.roundtrip, dtype, x