diff --git a/skimage/novice/_novice.py b/skimage/novice/_novice.py index e58f1b78..553da070 100644 --- a/skimage/novice/_novice.py +++ b/skimage/novice/_novice.py @@ -1,6 +1,7 @@ import os import imghdr from collections import namedtuple +from io import BytesIO import numpy as np from skimage import io @@ -10,6 +11,9 @@ from skimage.color import color_dict from skimage.io.util import file_or_url_context, is_url import six +from six.moves.urllib_parse import urlparse +from six.moves.urllib import request +urlopen = request.urlopen # Convert colors from `skimage.color` to uint8 and allow access through # dict or a named tuple. @@ -72,15 +76,18 @@ class Pixel(object): Vertical coordinate of this pixel (bottom = 0). rgb : tuple RGB tuple with red, green, and blue components (0-255) + alpha : int + Transparency component (0-255), 255 (opaque) by default """ - def __init__(self, pic, array, x, y, rgb): + def __init__(self, pic, array, x, y, rgb, alpha=255): self._picture = pic self._x = x self._y = y self._red = self._validate(rgb[0]) self._green = self._validate(rgb[1]) self._blue = self._validate(rgb[2]) + self._alpha = self._validate(alpha) @property def x(self): @@ -122,6 +129,16 @@ class Pixel(object): self._blue = self._validate(value) self._setpixel() + @property + def alpha(self): + """The transparency component of the pixel (0-255).""" + return self._alpha + + @alpha.setter + def alpha(self, value): + self._alpha = self._validate(value) + self._setpixel() + @property def rgb(self): """The RGB color components of the pixel (3 values 0-255).""" @@ -129,7 +146,25 @@ class Pixel(object): @rgb.setter def rgb(self, value): - self._red, self._green, self._blue = (self._validate(v) for v in value) + if len(value) == 4: + self.rgba = value + else: + self._red, self._green, self._blue \ + = (self._validate(v) for v in value) + self._alpha = 255 + self._setpixel() + + @property + def rgba(self): + """The RGB color and transparency components of the pixel + (4 values 0-255). + """ + return (self.red, self.green, self.blue, self.alpha) + + @rgba.setter + def rgba(self, value): + self._red, self._green, self._blue, self._alpha \ + = (self._validate(v) for v in value) self._setpixel() def _validate(self, value): @@ -145,16 +180,17 @@ class Pixel(object): return value def _setpixel(self): - self._picture.xy_array[self._x, self._y] = self.rgb + # RGB + alpha + self._picture.xy_array[self._x, self._y] = self.rgba self._picture._array_modified() def __eq__(self, other): if isinstance(other, Pixel): - return self.rgb == other.rgb + return self.rgba == other.rgba def __repr__(self): - args = self.red, self.green, self.blue - return "Pixel(red={0}, green={1}, blue={2})".format(*args) + args = self.red, self.green, self.blue, self.alpha + return "Pixel(red={0}, green={1}, blue={2}, alpha={3})".format(*args) class Picture(object): @@ -165,9 +201,9 @@ class Picture(object): path : str Path to an image file to load / URL of an image array : array - Raw RGB image data [0-255], with origin at top-left. + Raw RGB or RGBA image data [0-255], with origin at top-left. xy_array : array - Raw RGB image data [0-255], with origin at bottom-left. + Raw RGB or RGBA image data [0-255], with origin at bottom-left. Examples -------- @@ -218,13 +254,17 @@ class Picture(object): path = os.path.abspath(path) self._path = path with file_or_url_context(path) as context: - self.array = io.imread(context) + self.array = img_as_ubyte(io.imread(context)) self._format = imghdr.what(context) elif array is not None: self.array = array elif xy_array is not None: self.xy_array = xy_array + # Force RGBA internally (use max alpha) + if self.array.shape[-1] == 3: + self.array = np.insert(self.array, 3, values=255, axis=2) + @staticmethod def from_size(size, color='black'): """Return a Picture of the specified size and a uniform color. @@ -234,13 +274,18 @@ class Picture(object): size : tuple Width and height of the picture in pixels. color : tuple or str - RGB tuple with the fill color for the picture [0-255] or a valid - key in `color_dict`. + RGB or RGBA tuple with the fill color for the picture [0-255] or + a valid key in `color_dict`. """ if isinstance(color, six.string_types): color = color_dict[color] - rgb_size = tuple(size) + (3,) + rgb_size = tuple(size) + (len(color),) array = np.ones(rgb_size, dtype=np.uint8) * color + + # Force RGBA internally (use max alpha) + if array.shape[-1] == 3: + array = np.insert(array, 3, values=255, axis=2) + return Picture(array=array) @property @@ -384,13 +429,31 @@ class Picture(object): def blue(self, value): self._set_channel(2, value) + @property + def alpha(self): + """The transparency component of the pixel (0-255).""" + return self._get_channel(3).ravel() + + @alpha.setter + def alpha(self, value): + self._set_channel(3, value) + @property def rgb(self): """The RGB color components of the pixel (3 values 0-255).""" - return self.xy_array + return self.xy_array[:, :, :3] @rgb.setter def rgb(self, value): + self.xy_array[:, :, :3] = value + + @property + def rgba(self): + """The RGBA color components of the pixel (4 values 0-255).""" + return self.xy_array + + @rgba.setter + def rgba(self, value): self.xy_array[:] = value def __iter__(self): diff --git a/skimage/novice/tests/test_novice.py b/skimage/novice/tests/test_novice.py index 6b4295b5..a1f750ab 100644 --- a/skimage/novice/tests/test_novice.py +++ b/skimage/novice/tests/test_novice.py @@ -13,8 +13,12 @@ IMAGE_PATH = os.path.join(data_dir, "chelsea.png") SMALL_IMAGE_PATH = os.path.join(data_dir, "block.png") -def _array_2d_to_RGB(array): - return np.tile(array[:, :, np.newaxis], (1, 1, 3)) +def _array_2d_to_RGBA(array): + return np.tile(array[:, :, np.newaxis], (1, 1, 4)) + + +def _array_2d_to_RGBA(array): + return np.tile(array[:, :, np.newaxis], (1, 1, 4)) def test_xy_to_array_origin(): @@ -82,6 +86,28 @@ def test_pixel_rgb(): for i, channel in enumerate((pixel.red, pixel.green, pixel.blue)): assert_equal(channel, i + 3) + pixel.rgb = np.arange(4) + assert_equal(pixel.rgb, np.arange(3)) + + +def test_pixel_rgba(): + pic = novice.Picture.from_size((3, 3), color=(10, 10, 10)) + pixel = pic[0, 0] + pixel.rgba = np.arange(4) + + assert_equal(pixel.rgba, np.arange(4)) + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue, pixel.alpha)): + assert_equal(channel, i) + + pixel.red = 3 + pixel.green = 4 + pixel.blue = 5 + pixel.alpha = 6 + assert_equal(pixel.rgba, np.arange(4) + 3) + + for i, channel in enumerate((pixel.red, pixel.green, pixel.blue, pixel.alpha)): + assert_equal(channel, i + 3) + def test_pixel_rgb_float(): pixel = novice.Picture.from_size((1, 1))[0, 0] @@ -89,6 +115,12 @@ def test_pixel_rgb_float(): assert_equal(pixel.rgb, (1, 1, 1)) +def test_pixel_rgba_float(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.rgba = (1.1, 1.1, 1.1, 1.1) + assert_equal(pixel.rgba, (1, 1, 1, 1)) + + def test_modified_on_set(): pic = novice.Picture(SMALL_IMAGE_PATH) pic[0, 0] = (1, 1, 1) @@ -161,7 +193,7 @@ def test_indexing(): def test_picture_slice(): - array = _array_2d_to_RGB(np.arange(0, 10)[np.newaxis, :]) + array = _array_2d_to_RGBA(np.arange(0, 10)[np.newaxis, :]) pic = novice.Picture(array=array) x_slice = slice(3, 8) @@ -171,7 +203,7 @@ def test_picture_slice(): def test_move_slice(): h, w = 3, 12 - array = _array_2d_to_RGB(np.linspace(0, 255, h * w).reshape(h, w)) + array = _array_2d_to_RGBA(np.linspace(0, 255, h * w).reshape(h, w)) array = array.astype(np.uint8) pic = novice.Picture(array=array) @@ -191,7 +223,7 @@ def test_move_slice(): def test_negative_index(): n = 10 - array = _array_2d_to_RGB(np.arange(0, n)[np.newaxis, :]) + array = _array_2d_to_RGBA(np.arange(0, n)[np.newaxis, :]) # Test both x and y indices. pic = novice.Picture(array=array) assert pic[-1, 0] == pic[n - 1, 0] @@ -201,7 +233,7 @@ def test_negative_index(): def test_negative_slice(): n = 10 - array = _array_2d_to_RGB(np.arange(0, n)[np.newaxis, :]) + array = _array_2d_to_RGBA(np.arange(0, n)[np.newaxis, :]) # Test both x and y slices. pic = novice.Picture(array=array) assert pic[-3:, 0] == pic[n - 3:, 0] @@ -211,7 +243,7 @@ def test_negative_slice(): def test_getitem_with_step(): h, w = 5, 5 - array = _array_2d_to_RGB(np.linspace(0, 255, h * w).reshape(h, w)) + array = _array_2d_to_RGBA(np.linspace(0, 255, h * w).reshape(h, w)) pic = novice.Picture(array=array) sliced_pic = pic[::2, ::2] assert sliced_pic == novice.Picture(array=array[::2, ::2]) @@ -271,5 +303,11 @@ def test_pixel_blue_raises(): pixel.blue = 256 +@raises(ValueError) +def test_pixel_alpha_raises(): + pixel = novice.Picture.from_size((1, 1))[0, 0] + pixel.alpha = 256 + + if __name__ == '__main__': np.testing.run_module_suite()