From f6761524481c163ed72e8cbdadec98776192c31f Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:18:09 -0500 Subject: [PATCH 01/15] add stefan's image class --- skimage/io/_io.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 477fe8bc..85e5c005 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -5,10 +5,64 @@ from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np + # Shared image queue _image_stack = [] +class Image(np.ndarray): + """Image data with tags.""" + + tags = {'filename': '', + 'EXIF': {}, + 'info': {}} + + def __new__(image_cls, arr, **kwargs): + """Set the image data and tags according to given parameters. + + Input: + ------ + `image_cls` : Image class specification + This is not normally specified by the user. + `arr` : ndarray + Image data. + ``**kwargs`` : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + """ + x = np.asarray(arr).view(image_cls) + for tag, value in Image.tags.items(): + setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) + return x + + def __array_finalize__(self, obj): + """Copy object tags.""" + for tag, value in Image.tags.items(): + setattr(self, tag, getattr(obj, tag, value)) + return + + def __reduce__(self): + object_state = list(np.ndarray.__reduce__(self)) + subclass_state = {} + for tag in self.tags: + subclass_state[tag] = getattr(self, tag) + object_state[2] = (object_state[2], subclass_state) + return tuple(object_state) + + def __setstate__(self, state): + nd_state, subclass_state = state + np.ndarray.__setstate__(self, nd_state) + + for tag in subclass_state: + setattr(self, tag, subclass_state[tag]) + + @property + def exposure(self): + """Return exposure time based on EXIF tag.""" + exposure = self.EXIF['EXIF ExposureTime'].values[0] + return exposure.num / float(exposure.den) + + def push(img): """Push an image onto the shared image stack. @@ -77,7 +131,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return img + return Image(img) def imread_collection(load_pattern, conserve_memory=True, From d2e04848453696acb68ddfea57ab455b6c8cc9a6 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:19:22 -0500 Subject: [PATCH 02/15] add html repr method for Image class --- skimage/io/_io.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 85e5c005..3e5bb670 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,10 +1,17 @@ __all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] +import base64 + from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np +try: + import cStringIO as StringIO +except ImportError: + import StringIO + # Shared image queue _image_stack = [] @@ -49,6 +56,12 @@ class Image(np.ndarray): object_state[2] = (object_state[2], subclass_state) return tuple(object_state) + def _repr_html_(self): + str_buffer = StringIO.StringIO() + imsave(str_buffer, self, format_str='png') + base64_str = base64.b64encode(str_buffer.getvalue()) + return '' % base64_str + def __setstate__(self, state): nd_state, subclass_state = state np.ndarray.__setstate__(self, nd_state) From 605a4e2cd73e4ec1e2ff49ccd18625f06a93b1e5 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:22:53 -0500 Subject: [PATCH 03/15] add support for serializing to file-like objects (e.g. StringIO) to PIL plugin --- skimage/io/_plugins/pil_plugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 96106c5c..f42efe89 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -59,17 +59,21 @@ def _palette_is_grayscale(pil_image): return np.allclose(np.diff(valid_palette), 0) -def imsave(fname, arr): +def imsave(fname, arr, format_str=None): """Save an image to disk. Parameters ---------- - fname : str + 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 required if using a file-like object; + this is optional if fname is a string and the format can be + derived from the extension. Notes ----- @@ -101,7 +105,11 @@ def imsave(fname, arr): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - img.save(fname) + + if isinstance(fname, basestring): + img.save(fname, format=format_str) + elif callable(getattr(fname, 'write', None)): + img.save(fname, format=format_str) def imshow(arr): From 70cf6cfba0040dc347a31c75de4b0b06a2bd7bd0 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 15:39:50 -0500 Subject: [PATCH 04/15] fix test so it looks for new Image class --- skimage/io/_io.py | 2 +- skimage/io/tests/test_collection.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 3e5bb670..f3184588 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,4 +1,4 @@ -__all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] import base64 diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 0d420ae7..bf183f2b 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -7,6 +7,7 @@ from numpy.testing.decorators import skipif from skimage import data_dir from skimage.io import ImageCollection, MultiImage +from skimage.io import Image as ioImage try: @@ -33,7 +34,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is np.ndarray + assert type(self.collection[i]) is ioImage assert_array_almost_equal(self.collection[0], self.collection[-num]) From 32d3e1b161a12c46fa5c52a66c4bccd705c572b3 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 16:44:05 -0500 Subject: [PATCH 05/15] use repr_png and repr_jpeg hooks rather than repr_html these will work in the ipython qtconsole as well! --- skimage/io/_io.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index f3184588..c271f25b 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -56,11 +56,15 @@ class Image(np.ndarray): object_state[2] = (object_state[2], subclass_state) return tuple(object_state) - def _repr_html_(self): + def _repr_png_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='png') - base64_str = base64.b64encode(str_buffer.getvalue()) - return '' % base64_str + return str_buffer.getvalue() + + def _repr_jpeg_(self): + str_buffer = StringIO.StringIO() + imsave(str_buffer, self, format_str='jpeg') + return str_buffer.getvalue() def __setstate__(self, state): nd_state, subclass_state = state From 74cde286b405a06480efba046242e79f92b20882 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 20:33:25 -0500 Subject: [PATCH 06/15] allow qt_plugin imsave() function to write to file-like objects --- skimage/io/_plugins/qt_plugin.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index dd4e4a8f..401589a9 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -7,8 +7,8 @@ import sys window_manager.acquire('qt') try: - from PyQt4.QtGui import (QApplication, QMainWindow, QImage, QPixmap, - QLabel, QWidget) + from PyQt4.QtGui import (QApplication, QImage, + QLabel, QMainWindow, QPixmap, QWidget) from PyQt4 import QtCore, QtGui import sip import warnings @@ -148,12 +148,21 @@ def _app_show(): print 'No images to show. See `imshow`.' -def imsave(filename, img): +def imsave(filename, img, format_str=None): # we can add support for other than 3D uint8 here... img = prepare_for_display(img) qimg = QImage(img.data, img.shape[1], img.shape[0], img.strides[0], QImage.Format_RGB888) - saved = qimg.save(filename) + if _is_filelike(filename): + byte_array = QtCore.QByteArray() + qbuffer = QtCore.QBuffer() + qbuffer.open(QtCore.QIODevice.ReadWrite) + saved = qimg.save(qbuffer, format_str.upper()) + qbuffer.seek(0) + filename.write(qbuffer.readAll()) + qbuffer.close() + else: + saved = qimg.save(filename) if not saved: from textwrap import dedent msg = dedent( @@ -161,3 +170,7 @@ def imsave(filename, img): for the QT imsave plugin are: BMP, JPG, JPEG, PNG, PPM, TIFF, XBM, XPM''') raise RuntimeError(msg) + + +def _is_filelike(possible_filelike): + return callable(getattr(possible_filelike, 'write', None)) From fbb2ec3afab89c0d3866edcae7f636df32bdd9bd Mon Sep 17 00:00:00 2001 From: wilsaj Date: Fri, 20 Jul 2012 20:34:12 -0500 Subject: [PATCH 07/15] close temp StringIO buffers when we're done with them --- skimage/io/_io.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index c271f25b..514b828c 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -59,12 +59,16 @@ class Image(np.ndarray): def _repr_png_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='png') - return str_buffer.getvalue() + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str def _repr_jpeg_(self): str_buffer = StringIO.StringIO() imsave(str_buffer, self, format_str='jpeg') - return str_buffer.getvalue() + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str def __setstate__(self, state): nd_state, subclass_state = state From dcb7dacc6c5f37e9ab534a7e289044db4c77c010 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sat, 21 Jul 2012 10:23:20 -0500 Subject: [PATCH 08/15] open buffer on on byte_array --- skimage/io/_plugins/qt_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index 401589a9..bb4cf232 100644 --- a/skimage/io/_plugins/qt_plugin.py +++ b/skimage/io/_plugins/qt_plugin.py @@ -155,7 +155,7 @@ def imsave(filename, img, format_str=None): img.strides[0], QImage.Format_RGB888) if _is_filelike(filename): byte_array = QtCore.QByteArray() - qbuffer = QtCore.QBuffer() + qbuffer = QtCore.QBuffer(byte_array) qbuffer.open(QtCore.QIODevice.ReadWrite) saved = qimg.save(qbuffer, format_str.upper()) qbuffer.seek(0) From a31e0d9eeb83f03d113c254bd12df9d9bcf721e9 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:01:51 -0500 Subject: [PATCH 09/15] remove superfluous conditional logic --- skimage/io/_plugins/pil_plugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index f42efe89..2247f3a1 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -105,11 +105,7 @@ def imsave(fname, arr, format_str=None): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - - if isinstance(fname, basestring): - img.save(fname, format=format_str) - elif callable(getattr(fname, 'write', None)): - img.save(fname, format=format_str) + img.save(fname, format=format_str) def imshow(arr): From 7368332df97f94b67b625a0837c4164122f879d4 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:07:44 -0500 Subject: [PATCH 10/15] remove unused import --- skimage/io/_io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 514b828c..29076433 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,8 +1,6 @@ __all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] -import base64 - from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey import numpy as np From b9d468b6686eb40af29ad5c24b2dd6afa3e06542 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:20:22 -0500 Subject: [PATCH 11/15] use standard conventions for Image.__new__() cls attribute and docstring --- skimage/io/_io.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 29076433..8320ef9f 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -22,20 +22,18 @@ class Image(np.ndarray): 'EXIF': {}, 'info': {}} - def __new__(image_cls, arr, **kwargs): + def __new__(cls, arr, **kwargs): """Set the image data and tags according to given parameters. Input: ------ - `image_cls` : Image class specification - This is not normally specified by the user. - `arr` : ndarray + arr : ndarray Image data. - ``**kwargs`` : Image tags as keywords + kwargs : Image tags as keywords Specified in the form ``tag0=value``, ``tag1=value``. """ - x = np.asarray(arr).view(image_cls) + x = np.asarray(arr).view(cls) for tag, value in Image.tags.items(): setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) return x From 2b66f0c30314a8043efe0b8d5c9374b1468a3689 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 24 Jul 2012 23:21:42 -0500 Subject: [PATCH 12/15] role duplicate logic for Image._repr_png_() and Image._repr_jpeg_() into a common method --- skimage/io/_io.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 8320ef9f..95cb5abc 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -53,15 +53,14 @@ class Image(np.ndarray): return tuple(object_state) def _repr_png_(self): - str_buffer = StringIO.StringIO() - imsave(str_buffer, self, format_str='png') - return_str = str_buffer.getvalue() - str_buffer.close() - return return_str + return self._repr_image_format('png') def _repr_jpeg_(self): + return self._repr_image_format('jpeg') + + def _repr_image_format(self, format_str): str_buffer = StringIO.StringIO() - imsave(str_buffer, self, format_str='jpeg') + imsave(str_buffer, self, format_str=format_str) return_str = str_buffer.getvalue() str_buffer.close() return return_str From 9bd445700a059b5b861f10d7ff78a2cf0bd0ed75 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sun, 12 Aug 2012 18:20:58 -0500 Subject: [PATCH 13/15] add note about to display protocol to Image docstring --- skimage/io/_io.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 95cb5abc..20337797 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -16,7 +16,11 @@ _image_stack = [] class Image(np.ndarray): - """Image data with tags.""" + """Class representing Image data. + + These objects have tags for image metadata and IPython display protocol + methods for image display. + """ tags = {'filename': '', 'EXIF': {}, From fabedb58ec4bcdb24dd816c3a405779adff7cf22 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Sun, 12 Aug 2012 18:54:43 -0500 Subject: [PATCH 14/15] add unit test for Image._repr_png_() with PIL plugin --- skimage/io/tests/test_pil.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index e442c856..fa62e1dd 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -33,6 +33,7 @@ def setup_module(self): pass + @skipif(not PIL_available) def test_imread_flatten(): # a color image is flattened @@ -78,6 +79,20 @@ def test_imread_uint16(): assert_array_almost_equal(img, expected) +@skipif(not PIL_available) +def test_repr_png(): + img_path = os.path.join(data_dir, 'camera.png') + original_img = imread(img_path) + original_img_str = original_img._repr_png_() + + with NamedTemporaryFile(suffix='.png', mode='r+') as temp_png: + temp_png.write(original_img_str) + temp_png.seek(0) + round_trip = imread(temp_png) + + assert np.all(original_img == round_trip) + + # Big endian images not correctly loaded for PIL < 1.1.7 # Renable test when PIL 1.1.7 is more common. @skipif(True) From 38a96b9ac9af7c65b2ac1bf33f28daccaa5c4e23 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 21 Aug 2012 20:50:52 -0500 Subject: [PATCH 15/15] Remove tag metadata-related fucntions from Image class --- skimage/io/_io.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 20337797..71002395 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -42,20 +42,6 @@ class Image(np.ndarray): setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) return x - def __array_finalize__(self, obj): - """Copy object tags.""" - for tag, value in Image.tags.items(): - setattr(self, tag, getattr(obj, tag, value)) - return - - def __reduce__(self): - object_state = list(np.ndarray.__reduce__(self)) - subclass_state = {} - for tag in self.tags: - subclass_state[tag] = getattr(self, tag) - object_state[2] = (object_state[2], subclass_state) - return tuple(object_state) - def _repr_png_(self): return self._repr_image_format('png') @@ -69,19 +55,6 @@ class Image(np.ndarray): str_buffer.close() return return_str - def __setstate__(self, state): - nd_state, subclass_state = state - np.ndarray.__setstate__(self, nd_state) - - for tag in subclass_state: - setattr(self, tag, subclass_state[tag]) - - @property - def exposure(self): - """Return exposure time based on EXIF tag.""" - exposure = self.EXIF['EXIF ExposureTime'].values[0] - return exposure.num / float(exposure.den) - def push(img): """Push an image onto the shared image stack.