diff --git a/scikits/image/io/__init__.py b/scikits/image/io/__init__.py index 3ef9e1e1..da516861 100644 --- a/scikits/image/io/__init__.py +++ b/scikits/image/io/__init__.py @@ -1,7 +1,11 @@ """Utilities to read and write images in various formats.""" +import pil_plugin +import matplotlib_plugin + +from plugin import register as register_plugin -from pil_imread import * from sift import * from collection import * +from io import * diff --git a/scikits/image/io/collection.py b/scikits/image/io/collection.py index 7cf98e09..cf065f93 100644 --- a/scikits/image/io/collection.py +++ b/scikits/image/io/collection.py @@ -8,7 +8,7 @@ from glob import glob import os.path import numpy as np -from pil_imread import imread +from io import imread from PIL import Image @@ -266,7 +266,8 @@ 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): + if (self.conserve_memory and n != self._cached) or \ + (self.data[idx] is None): self.data[idx] = imread(self.files[n], self.as_grey, dtype=self._dtype) self._cached = n diff --git a/scikits/image/io/io.py b/scikits/image/io/io.py new file mode 100644 index 00000000..11369bec --- /dev/null +++ b/scikits/image/io/io.py @@ -0,0 +1,90 @@ +__all__ = ['imread', 'imsave', 'imshow'] + +from scikits.image.io import plugin as _plugin + +def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None, + **plugin_args): + """Load an image from file. + + Parameters + ---------- + fname : string + Image file name, e.g. ``test.jpg``. + as_grey : bool + If True, convert color images to grey-scale. If `dtype` is not given, + converted color images are returned as 32-bit float images. + Images that are already in grey-scale format are not converted. + dtype : dtype, optional + NumPy data-type specifier. If given, the returned image has this type. + If None (default), the data-type is determined automatically. + plugin : str + Name of plugin to use. By default, the different plugins are + tried (starting with the Python Imaging Library) until a suitable + candidate is found. + + Other Parameters + ---------------- + flatten : bool + Backward compatible keyword, superseded by `as_grey`. + + Returns + ------- + img_array : ndarray + The different colour bands/channels are stored in the + third dimension, such that a grey-image is MxN, an + RGB-image MxNx3 and an RGBA-image MxNx4. + + Other parameters + ---------------- + plugin_args : keywords + Passed to the given plugin. + + """ + # Backward compatibility + if flatten is not None: + as_grey = flatten + + return _plugin.call('read', fname, as_grey=as_grey, dtype=dtype, + plugin=plugin, **plugin_args) + +def imsave(fname, arr, plugin=None, **plugin_args): + """Save an image to file. + + Parameters + ---------- + fname : str + Target filename. + arr : ndarray of shape (M,N) or (M,N,3) or (M,N,4) + Image data. + plugin : str + Name of plugin to use. By default, the different plugins are + tried (starting with the Python Imaging Library) until a suitable + candidate is found. + + Other parameters + ---------------- + plugin_args : keywords + Passed to the given plugin. + + """ + return _plugin.call('save', fname, arr, plugin=plugin, **plugin_args) + +def imshow(arr, plugin=None, **plugin_args): + """Display an image. + + Parameters + ---------- + arr : ndarray + Image data. + plugin : str + Name of plugin to use. By default, the different plugins are + tried (starting with the Python Imaging Library) until a suitable + candidate is found. + + Other parameters + ---------------- + plugin_args : keywords + Passed to the given plugin. + + """ + return _plugin.call('show', arr, plugin=plugin, **plugin_args) diff --git a/scikits/image/io/matplotlib_plugin.py b/scikits/image/io/matplotlib_plugin.py new file mode 100644 index 00000000..3d748fe5 --- /dev/null +++ b/scikits/image/io/matplotlib_plugin.py @@ -0,0 +1,8 @@ +import plugin + +try: + import matplotlib.pyplot as plt +except ImportError: + pass +else: + plugin.register('matplotlib', show=plt.imshow, save=plt.imsave) diff --git a/scikits/image/io/pil_imread.py b/scikits/image/io/pil_imread.py deleted file mode 100644 index 10d273ca..00000000 --- a/scikits/image/io/pil_imread.py +++ /dev/null @@ -1,67 +0,0 @@ -__all__ = ['imread'] - -import numpy as np - -def imread(fname, flatten=False, dtype=None): - """Load an image from file. - - Parameters - ---------- - fname : string - Image file name, e.g. ``test.jpg``. - flatten : bool - If True, convert color images to grey-scale. If `dtype` is not given, - converted color images are returned as 32-bit float images. - Images that are already in grey-scale format are not converted. - dtype : dtype, optional - NumPy data-type specifier. If given, the returned image has this type. - If None (default), the data-type is determined automatically. - - Returns - ------- - img_array : ndarray - The different colour bands/channels are stored in the - third dimension, such that a grey-image is MxN, an - RGB-image MxNx3 and an RGBA-image MxNx4. - - """ - try: - from PIL import Image - except ImportError: - raise ImportError("Could not import the Python Imaging Library (PIL)" - " required to load image files. Please refer to" - " http://pypi.python.org/pypi/PIL/ for installation" - " instructions.") - - im = Image.open(fname) - if im.mode == 'P': - if palette_is_grayscale(im): - im = im.convert('L') - else: - im = im.convert('RGB') - if flatten and not im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'): - im = im.convert('F') - return np.array(im, dtype=dtype) - - -def palette_is_grayscale(pil_image): - """Return True if PIL image in palette mode is grayscale. - - 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) diff --git a/scikits/image/io/pil_plugin.py b/scikits/image/io/pil_plugin.py new file mode 100644 index 00000000..aa8adb74 --- /dev/null +++ b/scikits/image/io/pil_plugin.py @@ -0,0 +1,54 @@ +__all__ = ['imread'] + +import numpy as np +import plugin + +try: + from PIL import Image + has_pil = True +except ImportError: + has_pil = False + +def imread(fname, as_grey=False, dtype=None): + """Load an image from file. + + """ + im = Image.open(fname) + if im.mode == 'P': + if palette_is_grayscale(im): + im = im.convert('L') + else: + im = im.convert('RGB') + + if as_grey and not \ + im.mode in ('1', 'L', 'I', 'F', 'I;16', 'I;16L', 'I;16B'): + im = im.convert('F') + + return np.array(im, dtype=dtype) + +def palette_is_grayscale(pil_image): + """Return True if PIL image in palette mode is grayscale. + + 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) + + +if has_pil: + plugin.register('PIL', read=imread) diff --git a/scikits/image/io/plugin.py b/scikits/image/io/plugin.py new file mode 100644 index 00000000..42146bfc --- /dev/null +++ b/scikits/image/io/plugin.py @@ -0,0 +1,143 @@ +"""Handle image reading, writing and plotting plugins. + +""" + +__all__ = ['register', 'use'] + +import warnings + +plugin_store = {'read': [], + 'save': [], + 'show': []} + +def register(name, **kwds): + """Register an image I/O plugin. + + Parameters + ---------- + name : str + Name of this plugin. + read : callable, optional + Function with signature + ``read(filename, as_grey=False, dtype=None, **plugin_specific_args)`` + that reads images. + save : callable, optional + Function with signature + ``write(filename, arr, **plugin_specific_args)`` + that writes an image to disk. + show : callable, optional + Function with signature + ``show(X, **plugin_specific_args)`` that displays an image. + + """ + for kind in kwds: + if kind not in plugin_store.keys(): + raise ValueError('Tried to register invalid plugin method.') + + func = kwds[kind] + if not callable(func): + raise ValueError('Can only register functions as plugins.') + + plugin_store[kind].append((name, func)) + + +def call(kind, *args, **kwargs): + """Find the appropriate plugin of 'kind' and execute it. + + Parameters + ---------- + kind : {'show', 'save', 'read'} + Function to look up. + plugin : str, optional + Plugin to load. Defaults to None, in which case the first + matching plugin is used. + *args, **kwargs : arguments and keyword arguments + Passed to the plugin function. + + """ + if not kind in plugin_store: + raise ValueError('Invalid function (%s) requested.' % kind) + + plugin_funcs = plugin_store[kind] + if len(plugin_funcs) == 0: + raise RuntimeError('No suitable plugin registered for %s' % kind) + + plugin = kwargs.pop('plugin', None) + if plugin is None: + _, func = plugin_funcs[0] + else: + try: + func = [f for (p,f) in plugin_funcs if p == plugin][0] + except IndexError: + raise RuntimeError('Could not find the plugin "%s" for %s.' % \ + (plugin, kind)) + + return func(*args, **kwargs) + +def use(name, kind=None): + """Set the default plugin for a specified operation. + + Parameters + ---------- + name : str + Name of plugin. + kind : {'save', 'read', 'show'}, optional + Set the plugin for this function. By default, + the plugin is set for all functions. + + Examples + -------- + + Use Python Imaging Library to read images: + + >>> from scikits.image.io import plugin + >>> plugin.use('PIL', 'read') + + """ + if kind is None: + kind = plugin_store.keys() + else: + kind = [kind] + + for k in kind: + if not k in plugin_store: + raise RuntimeError("Could not find plugin for '%s'" % k) + + funcs = plugin_store[k] + + # Shuffle the plugins so that the requested plugin stands first + # in line + funcs = [(n, f) for (n, f) in funcs if n == name] + \ + [(n, f) for (n, f) in funcs if n != name] + + n, f = funcs[0] + if not n == name: + warnings.warn(RuntimeWarning('Could not set plugin "%s" for' + ' function "%s".' % (name, k))) + + plugin_store[k] = funcs + +def available(kind=None): + """List available plugins. + + Parameters + ---------- + kind : {'show', 'save', 'read'}, optional + Display the plugin list for the given function type. If not specified, + return a dictionary with the plugins for all functions. + + """ + if kind is None: + kind = plugin_store.keys() + else: + kind = [kind] + + d = {} + for k in kind: + if not k in plugin_store: + raise ValueError('No function "%s" exists in the plugin registry.' + % kind) + + d[k] = [name for (name, func) in plugin_store[k]] + + return d diff --git a/scikits/image/io/tests/test_imread.py b/scikits/image/io/tests/test_imread.py index 2e9a2f7a..ab8417d1 100644 --- a/scikits/image/io/tests/test_imread.py +++ b/scikits/image/io/tests/test_imread.py @@ -3,7 +3,7 @@ import numpy as np from scikits.image import data_dir from scikits.image.io import imread -from scikits.image.io.pil_imread import palette_is_grayscale +from scikits.image.io.pil_plugin import palette_is_grayscale def test_imread_flatten(): # a color image is flattened and returned as float32 diff --git a/scikits/image/io/tests/test_plugin.py b/scikits/image/io/tests/test_plugin.py new file mode 100644 index 00000000..987ea8c7 --- /dev/null +++ b/scikits/image/io/tests/test_plugin.py @@ -0,0 +1,52 @@ +from numpy.testing import * + +from scikits.image import io +from scikits.image.io import plugin + +from copy import deepcopy + +def read(fname, as_grey=False, dtype=None): + assert fname == 'test.png' + assert as_grey == True + assert dtype == 'i4' + +def save(fname, arr): + assert fname == 'test.png' + assert arr == [1, 2, 3] + +def show(arr, plugin_arg=None): + assert arr == [1, 2, 3] + assert plugin_arg == (1, 2) + +def show_other(arr): + return "other" + +def setup_module(self): + self.backup_plugin_store = deepcopy(plugin.plugin_store) + plugin.register('test', read=read, save=save, show=show) + plugin.register('other', show=show_other) + +def teardown_module(self): + plugin.plugin_store = self.backup_plugin_store + +class TestPlugin: + def test_read(self): + io.imread('test.png', as_grey=True, dtype='i4', plugin='test') + + def test_save(self): + io.imsave('test.png', [1, 2, 3], plugin='test') + + def test_show(self): + io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') + + def test_use(self): + plugin.use('other', 'show') + assert io.imshow(None) == 'other' + + def test_available(self): + plugin.use('other', 'show') + d = plugin.available('show') + assert d['show'][0] == 'other' + +if __name__ == "__main__": + run_module_suite()