diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 477fe8bc..71002395 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -1,14 +1,61 @@ -__all__ = ['imread', 'imread_collection', 'imsave', 'imshow', 'show', +__all__ = ['Image', 'imread', 'imread_collection', 'imsave', 'imshow', 'show', 'push', 'pop'] 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 = [] +class Image(np.ndarray): + """Class representing Image data. + + These objects have tags for image metadata and IPython display protocol + methods for image display. + """ + + tags = {'filename': '', + 'EXIF': {}, + 'info': {}} + + def __new__(cls, arr, **kwargs): + """Set the image data and tags according to given parameters. + + Input: + ------ + arr : ndarray + Image data. + kwargs : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + """ + 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 + + def _repr_png_(self): + 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=format_str) + return_str = str_buffer.getvalue() + str_buffer.close() + return return_str + + def push(img): """Push an image onto the shared image stack. @@ -77,7 +124,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, diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 96106c5c..2247f3a1 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,7 @@ def imsave(fname, arr): arr = arr.astype(np.uint8) img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) - img.save(fname) + img.save(fname, format=format_str) def imshow(arr): diff --git a/skimage/io/_plugins/qt_plugin.py b/skimage/io/_plugins/qt_plugin.py index dd4e4a8f..bb4cf232 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(byte_array) + 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)) diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index fb36bf70..2bbfefa7 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -8,6 +8,7 @@ from numpy.testing.decorators import skipif from skimage import data_dir from skimage.io import ImageCollection, MultiImage from skimage.io.collection import alphanumeric_key +from skimage.io import Image as ioImage try: @@ -54,7 +55,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]) 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)