This exercise borrows heavily from Matt Davis' exercise but doesn't write it out to a file, and doesn't use functions that a python novice maybe hasn't seen yet. It requires Matt's ipythonblocks, which you should already have installed, and the Python Image Library (PIL), which you may not have installed. If you need to install PIL, at the command line type:
conda install pil
from PIL import Image
First we need to read the image in.
im = Image.open('starry_night.jpg')
It would be nice to display the image in our notebook so we can see what we are doing to it along the way using PIL before we convert it into an ipythonblock table. IPython has a function that will let us do that. Unfortunately, its name is also Image
, so we need to rename it to something else when we import it, or the two Image
functions will conflict.
from IPython.core.display import Image as IPyImage
If we feed IPyImage
an image file, it displays it in the notebook:
IPyImage('starry_night.jpg')
PIL's Image
module has a function called .getdata()
that returns all the data in an image in raw
format, so if we feed it to IPyImage, it should display it, right?
IPyImage(data=im.getdata(), format=u'jpeg')
/Users/jeramiaory/anaconda/lib/python2.7/site-packages/IPython/core/formatters.py:249: FormatterWarning: image/jpeg formatter returned invalid type <type 'ImagingCore'> (expected (<type 'str'>, <type 'unicode'>)) for object: <IPython.core.display.Image object at 0x104b9d890> FormatterWarning
<IPython.core.display.Image at 0x104b9d890>
Oops. It turns out that none of the functions in PIL's Image
module will return data in a way that IPyImage
can understand.
Judging from the error message:
(expected (<type 'str'>, <type 'unicode'>))
IPyImage
wants something that looks like a string. To give IPyImage
a string, we can save the contents of im
to a virtual string using StringIO
. The IO
of StringIO
stands for Input/Output.
StringIO
is used to write a string to memory and read it out again instead of writing to a temporary file on the disk. This has the advantage of being much faster (memory is generally faster than a disk), but can get you in trouble if you try to write a very large file to memory. As the image is ~100 kb, we should be ok.
Once it is saved as a StringIO object, the .getvalue()
funtion of StringIO
will return something that IPyImage likes.
import StringIO
img_output = StringIO.StringIO() # initialize a new StringIO object called img_output
im.save(img_output, format="JPEG") # save the contents of im to our StringIO object
img_contents = img_output.getvalue() # read the values back out of img_contents
IPyImage(data=img_contents, format=u'jpeg') # display the image using IPython's built in display capability
The im object has a number of properties, for example, size:
im.size
(500, 398)
We could feed the data from the im object into an ipythonblock BlockGrid, but it would be a little unwieldy. So we can also resize the image. the Image.ANTIALIAS
part of the statement below means the resize function does the best job it can to make the smaller version look like the bigger version.
im = im.resize((125, 100), Image.ANTIALIAS)
im.size
(125, 100)
Now let's look at our image again.
img_output = StringIO.StringIO() # This reinitializes our virtual file
im.save(img_output, format="JPEG")
img_contents = img_output.getvalue()
IPyImage(data=img_contents, format=u'jpeg')
Aw, a little tiny baby image! To see what we did to the data content of the image, let's display it again, but scale it to the size of the original image using the 'width' and 'height' flags.
IPyImage(data=img_contents, format=u'jpeg', width=500, height=398)
Now we can start transferring the image data from the 'im' object to a new ipythonblocks grid. First we will import BlockGrid as before and create a blank grid with the same dimensions as the image. The 'block_size=4' helps us see what we're doing by making the blocks 4 pixels by 4 pixels, and 'lines_on=False' means we won't see the grid markers.
from ipythonblocks import BlockGrid
width = im.size[0]
height = im.size[1]
grid = BlockGrid(width, height, block_size=4, lines_on=False)
grid
The 'im.getpixel()' function returns the RGB values (tuple) for a given x & y.
im.getpixel.__doc__
'Get pixel value'
Ok, let's try it on a random x & y
im.getpixel(45,78)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-14-e70728ed6f86> in <module>() ----> 1 im.getpixel(45,78) TypeError: getpixel() takes exactly 2 arguments (3 given)
Wait, why didn't that work? And why does it say 3 arguments were given? It's because even though it wants the x and y position, it wants it as one argument, i.e. - a tuple consisting of two values. As for 3 vs 2, functions in python frequently count themselves (called self) as the first argument. So self + x + y = 3 arguments.
Ok, let's try it again.
im.getpixel((45,78))
(34, 48, 34)
So, to convert each RGB tuples in our PIL im object into an ipythonblock grid, we need a way of assigning the RGB value of each block while walking through all the blocks in the grid. By using our known height and width, we can accomplish this with a nested loop.
for row in range(0, height):
for column in range(0,width):
xy = (column, row)
grid[row,column]=im.getpixel(xy)
Questions:
Why do we flip column and row in the xy tuple above?
If the columns got from 0 to 124, and the value of width = 125, why does
range(0,width)
not return an out of index error?
grid
A
for row in range(0, height): for column in range(0,width): xy = (width - column - 1, row) grid[row,column]=im.getpixel(xy)
B
for row in range(0, height): for column in range(0,width): xy = (width - column - 1, height - row -1) grid[row,column]=im.getpixel(xy)
C
for row in range(height, 0): for column in range(width, 0): xy = (column, row) grid[row,column]=im.getpixel(xy)
D
for row in range(height - 1, 0, -1): for column in range(width - 1, 0, -1): xy = (width - column - 1, row) grid[row,column]=im.getpixel(xy)
Now that we have a series of tuples to play with, why not transform them somehow? We'll probably want to play around with it a few times, so we should create a function instead of retyping the whole thing. Here is an example:
def color_reduce(color_tuple, num_colors):
'''Convert a RGB color tuple to a reduced palette tuple.'''
color_div = 256 // int(num_colors ** (1 / 3.0))
red_value = (color_tuple[0] // color_div) * color_div
green_value = (color_tuple[1] // color_div) * color_div
blue_value = (color_tuple[2] // color_div) * color_div
return (red_value, green_value, blue_value)
Notice that many of the divisions in this function use //
instead of /
. This kind of division is called floor division. Floor division returns the integer of your answer only. So 1.5
becomes 1
, 0.999
becomes 0
, etc. Unfortunately, depending on the version of python you are using, //
and /
sometimes return the same thing.
print 6 / 8
# returns 0 in python 2.x
# returns 0.75 in python 3.x
print 6.0 / 8.0
# returns 0.75 in python 2.x and python 3.x
0 0.75
print 6 // 8
# returns 0 in python 2.x and 3.x
print 6.0 // 8.0
# returns 0.0 in python 2.x and 3.x
0 0.0
grid2 = BlockGrid(width, height, block_size=4, lines_on=False)
for row in range(0,height):
for column in range(0,width):
xy = (column,row)
newpixel = color_reduce(im.getpixel(xy), 256)
grid2[row,column] = newpixel
grid2
The 256 shades of gray go from black (0,0,0) to white (255,255,255). Create a grayscale version of starry night by converting the RGB values to a shade of gray.
hint: Just because someone's grascale version looks different from yours doesn't mean either of you are wrong.