From 9bec0ef8883c37d9871229df68035615f65ad6a4 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 31 Oct 2009 21:10:27 +0200 Subject: [PATCH 1/5] Plugin framework for image I/O. --- scikits/image/io/__init__.py | 6 +- scikits/image/io/io.py | 107 ++++++++++++++++++++++++++ scikits/image/io/matplotlib_plugin.py | 8 ++ scikits/image/io/pil_imread.py | 39 ---------- scikits/image/io/pil_plugin.py | 23 ++++++ scikits/image/io/plugin.py | 37 +++++++++ 6 files changed, 179 insertions(+), 41 deletions(-) create mode 100644 scikits/image/io/io.py create mode 100644 scikits/image/io/matplotlib_plugin.py delete mode 100644 scikits/image/io/pil_imread.py create mode 100644 scikits/image/io/pil_plugin.py create mode 100644 scikits/image/io/plugin.py diff --git a/scikits/image/io/__init__.py b/scikits/image/io/__init__.py index 3ef9e1e1..3107025f 100644 --- a/scikits/image/io/__init__.py +++ b/scikits/image/io/__init__.py @@ -1,7 +1,9 @@ """Utilities to read and write images in various formats.""" +import pil_plugin +import matplotlib_plugin -from pil_imread import * +from io import * +from plugin import register as register_plugin from sift import * from collection import * - diff --git a/scikits/image/io/io.py b/scikits/image/io/io.py new file mode 100644 index 00000000..e5a86a6f --- /dev/null +++ b/scikits/image/io/io.py @@ -0,0 +1,107 @@ +from scikits.image.io.plugin import plugin_store + +def _call_plugin(kind, *args, **kwargs): + 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 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 + + _call_plugin('read', as_grey, 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. + + """ + _call_plugin('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. + + """ + _call_plugin('show', arr, plugin=None, **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 421f45c7..00000000 --- a/scikits/image/io/pil_imread.py +++ /dev/null @@ -1,39 +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 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) diff --git a/scikits/image/io/pil_plugin.py b/scikits/image/io/pil_plugin.py new file mode 100644 index 00000000..2854baa3 --- /dev/null +++ b/scikits/image/io/pil_plugin.py @@ -0,0 +1,23 @@ +__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 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) + +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..3557e2d8 --- /dev/null +++ b/scikits/image/io/plugin.py @@ -0,0 +1,37 @@ +"""Handle image reading, writing and plotting plugins. + +""" + +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)) From a7b17bb21b59c22a3fbcb3b2e6e9f111863ddb75 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Nov 2009 11:46:12 +0200 Subject: [PATCH 2/5] Add tests for IO plugins. --- scikits/image/io/__init__.py | 3 +- scikits/image/io/io.py | 10 +++++-- scikits/image/io/tests/test_plugin.py | 40 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 scikits/image/io/tests/test_plugin.py diff --git a/scikits/image/io/__init__.py b/scikits/image/io/__init__.py index 3107025f..3c701ff1 100644 --- a/scikits/image/io/__init__.py +++ b/scikits/image/io/__init__.py @@ -3,7 +3,8 @@ import pil_plugin import matplotlib_plugin -from io import * from plugin import register as register_plugin from sift import * from collection import * + +from io import * diff --git a/scikits/image/io/io.py b/scikits/image/io/io.py index e5a86a6f..2ad75532 100644 --- a/scikits/image/io/io.py +++ b/scikits/image/io/io.py @@ -1,3 +1,5 @@ +__all__ = ['imread', 'imsave', 'imshow'] + from scikits.image.io.plugin import plugin_store def _call_plugin(kind, *args, **kwargs): @@ -20,6 +22,7 @@ def _call_plugin(kind, *args, **kwargs): return func(*args, **kwargs) + def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None, **plugin_args): """Load an image from file. @@ -62,7 +65,8 @@ def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - _call_plugin('read', as_grey, dtype, plugin=plugin, **plugin_args) + return _call_plugin('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. @@ -84,7 +88,7 @@ def imsave(fname, arr, plugin=None, **plugin_args): Passed to the given plugin. """ - _call_plugin('save', fname, arr, plugin=plugin, **plugin_args) + return _call_plugin('save', fname, arr, plugin=plugin, **plugin_args) def imshow(arr, plugin=None, **plugin_args): """Display an image. @@ -104,4 +108,4 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ - _call_plugin('show', arr, plugin=None, **plugin_args) + return _call_plugin('show', arr, plugin=plugin, **plugin_args) diff --git a/scikits/image/io/tests/test_plugin.py b/scikits/image/io/tests/test_plugin.py new file mode 100644 index 00000000..c39feeab --- /dev/null +++ b/scikits/image/io/tests/test_plugin.py @@ -0,0 +1,40 @@ +from numpy.testing import * + +from scikits.image import io +from scikits.image.io import plugin + +from copy import copy + +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) + +class TestPlugin: + def setup(self): + self.backup_plugin_store = copy(plugin.plugin_store) + plugin.register('test', read=read, save=save, show=show) + + 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 teardown(self): + plugin.plugin_store = self.backup_plugin_store + + +if __name__ == "__main__": + run_module_suite() From cdb1a1560f9cecc00a5f509ca2f3deff15b19a2e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Nov 2009 11:56:21 +0200 Subject: [PATCH 3/5] Restore plugin registry after testing. --- scikits/image/io/tests/test_plugin.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scikits/image/io/tests/test_plugin.py b/scikits/image/io/tests/test_plugin.py index c39feeab..85ae7694 100644 --- a/scikits/image/io/tests/test_plugin.py +++ b/scikits/image/io/tests/test_plugin.py @@ -3,7 +3,7 @@ from numpy.testing import * from scikits.image import io from scikits.image.io import plugin -from copy import copy +from copy import deepcopy def read(fname, as_grey=False, dtype=None): assert fname == 'test.png' @@ -18,11 +18,14 @@ def show(arr, plugin_arg=None): assert arr == [1, 2, 3] assert plugin_arg == (1, 2) -class TestPlugin: - def setup(self): - self.backup_plugin_store = copy(plugin.plugin_store) - plugin.register('test', read=read, save=save, show=show) +def setup_module(self): + self.backup_plugin_store = deepcopy(plugin.plugin_store) + plugin.register('test', read=read, save=save, show=show) +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') @@ -32,9 +35,5 @@ class TestPlugin: def test_show(self): io.imshow([1, 2, 3], plugin_arg=(1, 2), plugin='test') - def teardown(self): - plugin.plugin_store = self.backup_plugin_store - - if __name__ == "__main__": run_module_suite() From e7d6875cab75f136c97c36eee7b546bb08d223f1 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Nov 2009 11:59:10 +0200 Subject: [PATCH 4/5] Document _call_plugin. --- scikits/image/io/io.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scikits/image/io/io.py b/scikits/image/io/io.py index 2ad75532..c4d61f99 100644 --- a/scikits/image/io/io.py +++ b/scikits/image/io/io.py @@ -3,6 +3,19 @@ __all__ = ['imread', 'imsave', 'imshow'] from scikits.image.io.plugin import plugin_store def _call_plugin(kind, *args, **kwargs): + """Find the appropriate plugin of 'kind' and execute it. + + Parameters + ---------- + kind : {'imshow', 'imsave', 'imread'} + 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) From 940f318b318c8e9f8c7e2bfd448ac5971c7dd9b1 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Nov 2009 12:38:36 +0200 Subject: [PATCH 5/5] Remove plugin functionality from io.py. Implement setting default plugins and querying the plugins available. --- scikits/image/io/__init__.py | 1 + scikits/image/io/io.py | 42 +--------- scikits/image/io/plugin.py | 106 ++++++++++++++++++++++++++ scikits/image/io/tests/test_plugin.py | 13 ++++ 4 files changed, 124 insertions(+), 38 deletions(-) diff --git a/scikits/image/io/__init__.py b/scikits/image/io/__init__.py index 3c701ff1..da516861 100644 --- a/scikits/image/io/__init__.py +++ b/scikits/image/io/__init__.py @@ -4,6 +4,7 @@ import pil_plugin import matplotlib_plugin from plugin import register as register_plugin + from sift import * from collection import * diff --git a/scikits/image/io/io.py b/scikits/image/io/io.py index c4d61f99..11369bec 100644 --- a/scikits/image/io/io.py +++ b/scikits/image/io/io.py @@ -1,40 +1,6 @@ __all__ = ['imread', 'imsave', 'imshow'] -from scikits.image.io.plugin import plugin_store - -def _call_plugin(kind, *args, **kwargs): - """Find the appropriate plugin of 'kind' and execute it. - - Parameters - ---------- - kind : {'imshow', 'imsave', 'imread'} - 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) - +from scikits.image.io import plugin as _plugin def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None, **plugin_args): @@ -78,7 +44,7 @@ def imread(fname, as_grey=False, dtype=None, plugin=None, flatten=None, if flatten is not None: as_grey = flatten - return _call_plugin('read', fname, as_grey=as_grey, dtype=dtype, + return _plugin.call('read', fname, as_grey=as_grey, dtype=dtype, plugin=plugin, **plugin_args) def imsave(fname, arr, plugin=None, **plugin_args): @@ -101,7 +67,7 @@ def imsave(fname, arr, plugin=None, **plugin_args): Passed to the given plugin. """ - return _call_plugin('save', fname, arr, plugin=plugin, **plugin_args) + return _plugin.call('save', fname, arr, plugin=plugin, **plugin_args) def imshow(arr, plugin=None, **plugin_args): """Display an image. @@ -121,4 +87,4 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ - return _call_plugin('show', arr, plugin=plugin, **plugin_args) + return _plugin.call('show', arr, plugin=plugin, **plugin_args) diff --git a/scikits/image/io/plugin.py b/scikits/image/io/plugin.py index 3557e2d8..42146bfc 100644 --- a/scikits/image/io/plugin.py +++ b/scikits/image/io/plugin.py @@ -2,6 +2,10 @@ """ +__all__ = ['register', 'use'] + +import warnings + plugin_store = {'read': [], 'save': [], 'show': []} @@ -35,3 +39,105 @@ def register(name, **kwds): 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_plugin.py b/scikits/image/io/tests/test_plugin.py index 85ae7694..987ea8c7 100644 --- a/scikits/image/io/tests/test_plugin.py +++ b/scikits/image/io/tests/test_plugin.py @@ -18,9 +18,13 @@ 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 @@ -35,5 +39,14 @@ class TestPlugin: 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()