Files
scikit-image/skimage/novice/_novice.py
T
Johannes Schönberger 739dca0201 Fix novice doc test
2014-04-12 09:57:51 -04:00

495 lines
14 KiB
Python

import os
import imghdr
from collections import namedtuple
from io import BytesIO
import numpy as np
from skimage import io
from skimage import img_as_ubyte
from skimage.transform import resize
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.
color_dict = dict((name, tuple(int(255 * c + 0.5) for c in rgb))
for name, rgb in six.iteritems(color_dict))
colors = namedtuple('colors', color_dict.keys())(**color_dict)
def open(path):
"""Return Picture object from the given image path."""
return Picture(path)
def _verify_picture_index(index):
"""Raise error if picture index is not a 2D index/slice."""
if not (isinstance(index, tuple) and len(index) == 2):
raise IndexError("Expected 2D index but got {0!r}".format(index))
if all(isinstance(i, int) for i in index):
return index
# In case we need to fix the array index, convert tuple to list.
index = list(index)
for i, dim_slice in enumerate(index):
# If either index is a slice, ensure index object returns 2D array.
if isinstance(dim_slice, int):
index[i] = dim_slice = slice(dim_slice, dim_slice + 1)
return tuple(index)
def rgb_transpose(array):
"""Return RGB array with first 2 axes transposed."""
return np.transpose(array, (1, 0, 2))
def array_to_xy_origin(image):
"""Return view of image transformed from array to Cartesian origin."""
return rgb_transpose(image[::-1])
def xy_to_array_origin(image):
"""Return view of image transformed from Cartesian to array origin."""
return rgb_transpose(image[:, ::-1])
class Pixel(object):
"""A single pixel in a Picture.
Attributes
----------
pic : Picture
The Picture object that this pixel references.
array : array_like
Byte array with raw image data (RGB).
x : int
Horizontal coordinate of this pixel (left = 0).
y : int
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, 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):
"""Horizontal location of this pixel in the parent image(left = 0)."""
return self._x
@property
def y(self):
"""Vertical location of this pixel in the parent image (bottom = 0)."""
return self._y
@property
def red(self):
"""The red component of the pixel (0-255)."""
return self._red
@red.setter
def red(self, value):
self._red = self._validate(value)
self._setpixel()
@property
def green(self):
"""The green component of the pixel (0-255)."""
return self._green
@green.setter
def green(self, value):
self._green = self._validate(value)
self._setpixel()
@property
def blue(self):
"""The blue component of the pixel (0-255)."""
return self._blue
@blue.setter
def blue(self, value):
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)."""
return (self.red, self.green, self.blue)
@rgb.setter
def rgb(self, 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):
"""Verifies that the pixel value is in [0, 255]."""
try:
value = int(value)
if (value < 0) or (value > 255):
raise ValueError()
except ValueError:
msg = "Expected an integer between 0 and 255, but got {0} instead!"
raise ValueError(msg.format(value))
return value
def _setpixel(self):
# 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.rgba == other.rgba
def __repr__(self):
args = self.red, self.green, self.blue, self.alpha
return "Pixel(red={0}, green={1}, blue={2}, alpha={3})".format(*args)
class Picture(object):
"""A 2-D picture made up of pixels.
Attributes
----------
path : str
Path to an image file to load / URL of an image
array : array
Raw RGB or RGBA image data [0-255], with origin at top-left.
xy_array : array
Raw RGB or RGBA image data [0-255], with origin at bottom-left.
Examples
--------
Load an image from a file
>>> from skimage import novice
>>> from skimage import data
>>> picture = novice.open(data.data_dir + '/chelsea.png')
Load an image from a URL. URL must start with http(s):// or ftp(s)://
>>> picture = novice.open('http://scikit-image.org/_static/img/logo.png')
Create a blank 100 pixel wide, 200 pixel tall white image
>>> pic = Picture.from_size((100, 200), color=(255, 255, 255))
Use numpy to make an RGB byte array (shape is height x width x 3)
>>> import numpy as np
>>> data = np.zeros(shape=(200, 100, 3), dtype=np.uint8)
>>> data[:, :, 0] = 255 # Set red component to maximum
>>> pic = Picture(array=data)
Get the bottom-left pixel
>>> pic[0, 0]
Pixel(red=255, green=0, blue=0, alpha=255)
Get the top row of the picture
>>> pic[:, pic.height-1]
Picture(100 x 1)
Set the bottom-left pixel to black
>>> pic[0, 0] = (0, 0, 0)
Set the top row to red
>>> pic[:, pic.height-1] = (255, 0, 0)
"""
def __init__(self, path=None, array=None, xy_array=None):
self._modified = False
self.scale = 1
self._path = None
self._format = None
n_args = len([a for a in [path, array, xy_array] if a is not None])
if n_args != 1:
msg = "Must provide a single keyword arg (path, array, xy_array)."
ValueError(msg)
elif path is not None:
if not is_url(path):
path = os.path.abspath(path)
self._path = path
with file_or_url_context(path) as 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.
Parameters
----------
size : tuple
Width and height of the picture in pixels.
color : tuple or str
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) + (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
def array(self):
"""Image data stored as numpy array."""
return self._array
@array.setter
def array(self, array):
self._array = array
self._xy_array = array_to_xy_origin(array)
@property
def xy_array(self):
"""Image data stored as numpy array with origin at the bottom-left."""
return self._xy_array
@xy_array.setter
def xy_array(self, array):
self._xy_array = array
self._array = xy_to_array_origin(array)
def save(self, path):
"""Saves the picture to the given path.
Parameters
----------
path : str
Path (with file extension) where the picture is saved.
"""
io.imsave(path, self._rescale(self.array))
self._modified = False
self._path = os.path.abspath(path)
self._format = imghdr.what(path)
@property
def path(self):
"""The path to the picture."""
return self._path
@property
def modified(self):
"""True if the picture has changed."""
return self._modified
def _array_modified(self):
self._modified = True
self._path = None
@property
def format(self):
"""The image format of the picture."""
return self._format
@property
def size(self):
"""The size (width, height) of the picture."""
return self.xy_array.shape[:2]
@size.setter
def size(self, value):
# Don't resize if no change in size
if (value[0] != self.width) or (value[1] != self.height):
# skimage dimensions are flipped: y, x
new_size = (int(value[1]), int(value[0]))
new_array = resize(self.array, new_size, order=0)
self.array = img_as_ubyte(new_array)
self._array_modified()
@property
def width(self):
"""The width of the picture."""
return self.size[0]
@width.setter
def width(self, value):
self.size = (value, self.height)
@property
def height(self):
"""The height of the picture."""
return self.size[1]
@height.setter
def height(self, value):
self.size = (self.width, value)
def _repr_png_(self):
return io.Image(self._rescale(self.array))._repr_png_()
def show(self):
"""Display the image."""
io.imshow(self._rescale(self.array))
io.show()
def _makepixel(self, x, y):
"""Create a Pixel object for a given x, y location."""
rgb = self.xy_array[x, y]
return Pixel(self, self.array, x, y, rgb)
def _rescale(self, array):
"""Rescale image according to scale factor."""
if self.scale == 1:
return array
new_size = (self.height * self.scale, self.width * self.scale)
return img_as_ubyte(resize(array, new_size, order=0))
def _get_channel(self, channel):
"""Return a specific dimension out of the raw image data slice."""
return self._array[:, :, channel]
def _set_channel(self, channel, value):
"""Set a specific dimension in the raw image data slice."""
self._array[:, :, channel] = value
@property
def red(self):
"""The red component of the pixel (0-255)."""
return self._get_channel(0).ravel()
@red.setter
def red(self, value):
self._set_channel(0, value)
@property
def green(self):
"""The green component of the pixel (0-255)."""
return self._get_channel(1).ravel()
@green.setter
def green(self, value):
self._set_channel(1, value)
@property
def blue(self):
"""The blue component of the pixel (0-255)."""
return self._get_channel(2).ravel()
@blue.setter
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[:, :, :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):
"""Iterates over all pixels in the image."""
for x in range(self.width):
for y in range(self.height):
yield self._makepixel(x, y)
def __getitem__(self, xy_index):
"""Return `Picture`s for slices and `Pixel`s for indexes."""
xy_index = _verify_picture_index(xy_index)
if all(isinstance(index, int) for index in xy_index):
return self._makepixel(*xy_index)
else:
return Picture(xy_array=self.xy_array[xy_index])
def __setitem__(self, xy_index, value):
xy_index = _verify_picture_index(xy_index)
if isinstance(value, tuple):
self[xy_index].rgb = value
elif isinstance(value, Picture):
self.xy_array[xy_index] = value.xy_array
else:
raise TypeError("Invalid value type")
self._array_modified()
def __eq__(self, other):
if not isinstance(other, Picture):
raise NotImplementedError()
return np.all(self.array == other.array)
def __repr__(self):
return "Picture({0} x {1})".format(*self.size)
if __name__ == '__main__':
import doctest
doctest.testmod()