Merge pull request #600 from tonysyu/novice-package

Novice module
This commit is contained in:
Johannes Schönberger
2013-10-22 04:05:38 -07:00
6 changed files with 776 additions and 1 deletions
+3
View File
@@ -158,3 +158,6 @@
- Fedor Morozov
Drawing: Wu's anti-aliased circle
- Michael Hansen
novice submodule
+2 -1
View File
@@ -17,7 +17,8 @@ DEFAULT_COLORS = ('red', 'blue', 'yellow', 'magenta', 'green',
'indigo', 'darkorange', 'cyan', 'pink', 'yellowgreen')
color_dict = rgb_colors.__dict__
color_dict = dict((k, v) for k, v in six.iteritems(rgb_colors.__dict__)
if isinstance(v, tuple))
def _rgb_vector(color):
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

+75
View File
@@ -0,0 +1,75 @@
"""
skimage.novice
==============
A special Python image submodule for beginners.
Description
-----------
``skimage.novice`` provides a simple image manipulation interface for
beginners. It allows for easy loading, manipulating, and saving of image
files.
This module is primarily intended for teaching and differs significantly from
the normal, array-oriented image functions used by scikit-image.
.. note::
This module uses the Cartesian coordinate system, where the origin is at
the lower-left corner instead of the upper-right and the order is x, y
instead of row, column.
Example
-------
We can create a Picture object open opening an image file
>>> from skimage import novice
>>> from skimage import data
>>> picture = novice.open(data.data_dir + '/chelsea.png')
Pictures know their format
>>> print picture.format
png
... and where they came from
>>> print picture.path.endswith('chelsea.png')
True
... and their size
>>> print picture.size
(451, 300)
>>> print picture.width
451
Changing `size` resizes the picture.
>>> picture.size = (45, 30)
You can iterate over pixels, which have RGB values between 0 and 255,
and know their location in the picture.
>>> for pixel in picture:
... if (pixel.red > 128) and (pixel.x < picture.width):
... pixel.red /= 2
Pictures know if they've been modified from the original file
>>> print picture.modified
True
>>> print picture.path
None
Pictures can be indexed like arrays
>>> picture[0:20, 0:20] = (0, 0, 0)
Saving the picture updates the path attribute, format, and modified state.
>>> picture.save('save-demo.jpg')
>>> print picture.path.endswith('save-demo.jpg')
True
>>> print picture.format
jpeg
>>> print picture.modified
False
"""
from ._novice import Picture, open, colors, color_dict
__all__ = ['Picture', 'open', 'colors', 'color_dict']
+424
View File
@@ -0,0 +1,424 @@
import os
import imghdr
from collections import namedtuple
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._shared import six
# 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=os.path.abspath(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 {!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)
"""
def __init__(self, pic, array, x, y, rgb):
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])
@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 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):
self._red, self._green, self._blue = (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):
self._picture.xy_array[self._x, self._y] = self.rgb
self._picture._array_modified()
def __eq__(self, other):
if isinstance(other, Pixel):
return self.rgb == other.rgb
def __repr__(self):
args = self.red, self.green, self.blue
return "Pixel(red={0}, green={1}, blue={2})".format(*args)
class Picture(object):
"""A 2-D picture made up of pixels.
Attributes
----------
path : str
Path to an image file to load.
array : array
Raw RGB image data [0-255], with origin at top-left.
xy_array : array
Raw RGB 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')
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)
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:
self.array = img_as_ubyte(io.imread(path))
self._path = path
self._format = imghdr.what(path)
elif array is not None:
self.array = array
elif xy_array is not None:
self.xy_array = xy_array
@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 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,)
array = np.ones(rgb_size, dtype=np.uint8) * color
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 rgb(self):
"""The RGB color components of the pixel (3 values 0-255)."""
return self.xy_array
@rgb.setter
def rgb(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()
+272
View File
@@ -0,0 +1,272 @@
import os
import tempfile
import numpy as np
from numpy.testing import assert_equal, raises, assert_allclose
from skimage import novice
from skimage.novice._novice import (array_to_xy_origin, xy_to_array_origin,
rgb_transpose)
from skimage import data_dir
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 test_xy_to_array_origin():
h, w = 3, 5
array = np.arange(h * w).reshape(h, w, 1)
out = xy_to_array_origin(array_to_xy_origin(array.copy()))
assert np.allclose(out, array)
def test_pic_info():
pic = novice.open(IMAGE_PATH)
assert_equal(pic.format, "png")
assert_equal(pic.path, os.path.abspath(IMAGE_PATH))
assert_equal(pic.size, (451, 300))
assert_equal(pic.width, 451)
assert_equal(pic.height, 300)
assert not pic.modified
assert_equal(pic.scale, 1)
def test_pixel_iteration():
pic = novice.open(SMALL_IMAGE_PATH)
num_pixels = sum(1 for p in pic)
assert_equal(num_pixels, pic.width * pic.height)
def test_modify():
pic = novice.open(SMALL_IMAGE_PATH)
assert_equal(pic.modified, False)
for p in pic:
if p.x < (pic.width / 2):
p.red /= 2
p.green /= 2
p.blue /= 2
for p in pic:
if p.x < (pic.width / 2):
assert p.red <= 128
assert p.green <= 128
assert p.blue <= 128
s = pic.size
pic.size = (pic.width / 2, pic.height / 2)
assert_equal(pic.size, (int(s[0] / 2), int(s[1] / 2)))
assert pic.modified
assert pic.path is None
def test_pixel_rgb():
pic = novice.Picture.from_size((3, 3), color=(10, 10, 10))
pixel = pic[0, 0]
pixel.rgb = np.arange(3)
assert_equal(pixel.rgb, np.arange(3))
for i, channel in enumerate((pixel.red, pixel.green, pixel.blue)):
assert_equal(channel, i)
pixel.red = 3
pixel.green = 4
pixel.blue = 5
assert_equal(pixel.rgb, np.arange(3) + 3)
for i, channel in enumerate((pixel.red, pixel.green, pixel.blue)):
assert_equal(channel, i + 3)
def test_pixel_rgb_float():
pixel = novice.Picture.from_size((1, 1))[0, 0]
pixel.rgb = (1.1, 1.1, 1.1)
assert_equal(pixel.rgb, (1, 1, 1))
def test_modified_on_set():
pic = novice.Picture(SMALL_IMAGE_PATH)
pic[0, 0] = (1, 1, 1)
assert pic.modified
assert pic.path is None
def test_modified_on_set_pixel():
data = np.zeros(shape=(10, 5, 3), dtype=np.uint8)
pic = novice.Picture(array=data)
pixel = pic[0, 0]
pixel.green = 1
assert pic.modified
def test_update_on_save():
pic = novice.Picture(array=np.zeros((3, 3, 3)))
pic.size = (6, 6)
assert pic.modified
assert pic.path is None
with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp:
pic.save(tmp.name)
assert not pic.modified
assert_equal(pic.path, os.path.abspath(tmp.name))
assert_equal(pic.format, "jpeg")
def test_indexing():
array = 128 * np.ones((10, 10, 3), dtype=np.uint8)
pic = novice.Picture(array=array)
pic[0:5, 0:5] = (0, 0, 0)
for p in pic:
if (p.x < 5) and (p.y < 5):
assert_equal(p.rgb, (0, 0, 0))
assert_equal(p.red, 0)
assert_equal(p.green, 0)
assert_equal(p.blue, 0)
pic[:5, :5] = (255, 255, 255)
for p in pic:
if (p.x < 5) and (p.y < 5):
assert_equal(p.rgb, (255, 255, 255))
assert_equal(p.red, 255)
assert_equal(p.green, 255)
assert_equal(p.blue, 255)
pic[5:pic.width, 5:pic.height] = (255, 0, 255)
for p in pic:
if (p.x >= 5) and (p.y >= 5):
assert_equal(p.rgb, (255, 0, 255))
assert_equal(p.red, 255)
assert_equal(p.green, 0)
assert_equal(p.blue, 255)
pic[5:, 5:] = (0, 0, 255)
for p in pic:
if (p.x >= 5) and (p.y >= 5):
assert_equal(p.rgb, (0, 0, 255))
assert_equal(p.red, 0)
assert_equal(p.green, 0)
assert_equal(p.blue, 255)
def test_picture_slice():
array = _array_2d_to_RGB(np.arange(0, 10)[np.newaxis, :])
pic = novice.Picture(array=array)
x_slice = slice(3, 8)
subpic = pic[:, x_slice]
assert_allclose(subpic.array, array[x_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.astype(np.uint8)
pic = novice.Picture(array=array)
pic_orig = novice.Picture(array=array.copy())
# Move left cut of image to the right side.
cut = 5
rest = pic.width - cut
temp = pic[:cut, :]
temp.array = temp.array.copy()
pic[:rest, :] = pic[cut:, :]
pic[rest:, :] = temp
assert pic[rest:, :] == pic_orig[:cut, :]
assert pic[:rest, :] == pic_orig[cut:, :]
def test_negative_index():
n = 10
array = _array_2d_to_RGB(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]
pic = novice.Picture(array=rgb_transpose(array))
assert pic[0, -1] == pic[0, n - 1]
def test_negative_slice():
n = 10
array = _array_2d_to_RGB(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]
pic = novice.Picture(array=rgb_transpose(array))
assert pic[0, -3:] == pic[0, n - 3:]
def test_getitem_with_step():
h, w = 5, 5
array = _array_2d_to_RGB(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])
@raises(IndexError)
def test_1d_getitem_raises():
pic = novice.Picture.from_size((1, 1))
pic[1]
@raises(IndexError)
def test_3d_getitem_raises():
pic = novice.Picture.from_size((1, 1))
pic[1, 2, 3]
@raises(IndexError)
def test_1d_setitem_raises():
pic = novice.Picture.from_size((1, 1))
pic[1] = 0
@raises(IndexError)
def test_3d_setitem_raises():
pic = novice.Picture.from_size((1, 1))
pic[1, 2, 3] = 0
@raises(IndexError)
def test_out_of_bounds_indexing():
pic = novice.open(SMALL_IMAGE_PATH)
pic[pic.width, pic.height]
@raises(ValueError)
def test_pixel_rgb_raises():
pixel = novice.Picture.from_size((1, 1))[0, 0]
pixel.rgb = (-1, -1, -1)
@raises(ValueError)
def test_pixel_red_raises():
pixel = novice.Picture.from_size((1, 1))[0, 0]
pixel.red = 256
@raises(ValueError)
def test_pixel_green_raises():
pixel = novice.Picture.from_size((1, 1))[0, 0]
pixel.green = 256
@raises(ValueError)
def test_pixel_blue_raises():
pixel = novice.Picture.from_size((1, 1))[0, 0]
pixel.blue = 256
if __name__ == '__main__':
from numpy import testing
testing.run_module_suite()