Goal: to reduce an imported image to $k$ colours: colour quantisation, and write out as an array of classes corresponding to the $k$ colours, and an accompanying colour palette that reflects the original image.
We'll be using the Python Imaging Library, or rather the forked, updated, but backwards-compatible version Pillow
Use matplotlib
/pylab
inline with static images:
In [1]:
%pylab inline
Import Pillow
to work with images, and use the iPython display()
function.
In [2]:
from PIL import Image
from IPython.display import Image as show_image
iPython doesn't currently handle PIL
images directly, so we use a convenience function, adapted slightly from http://nbviewer.ipython.org/gist/deeplook/5162445:
In [3]:
from io import BytesIO
from IPython.core import display
def display_pil_image(im):
"""Displayhook function for PIL Images, rendered as PNG."""
b = BytesIO()
im.save(b, format='png')
data = b.getvalue()
ip_img = display.Image(data=data, format='png', embed=True)
return ip_img._repr_png_()
# register display func with PNG formatter:
png_formatter = get_ipython().display_formatter.formatters['image/png']
dpi = png_formatter.for_type(Image.Image, display_pil_image)
We'll be loading the image molecule.jpg
from the data
subdirectory. For convenience, we'll assign it into a variable here, along with the number of colours $k$ we want to posterise into:
In [4]:
image_filename = 'data/molecule.jpg' # Input image filename
k = 4 # Number of colours for posterising
We can preview it directly with iPython:
In [5]:
show_image(filename=image_filename)
Out[5]:
then load it with PIL
:
In [6]:
img = Image.open(image_filename)
img
Out[6]:
PIL
has an incompletely method for posterising images, the .quantize()
method.
In [7]:
help(img.quantize)
We can use this to generate a new, quantized image with $2 < k \leq 256$ colours. The maximum coverage
method seems to give a pleasing result here, but we'd need to use fast octree
for a .png
with transparency. Using .quantize()
as we are here will give us a palettised image: the $x$,$y$ image co-ordinates contain values that refer to rows in a lookup table of colours (the palette).
We could use the .convert()
method so that we keep the 'RGB'
mode, rather than generating a palettised image but, as we might want to consider the colour indices generated as 'classes', rather than colours, we keep the 'P'
palette mode, for now.
In [8]:
img_poster = img.quantize(colors=k, method=1)
img_poster
Out[8]:
Conversion of a PIL Image
object to a numpy
array
is a fairly simple operation, using PIL's array()
function.
In [9]:
img_array = array(img_poster)
Properties of the array can be inspected like any other numpy
array
:
In [10]:
img_array.size, img_array.shape, img_array.min(), img_array.max()
Out[10]:
The image is a 2D array, with 1024000 elements. These are arranged in 800 rows, and 1280 columns, with minimum value 0 and maximum value 3.
This reflects that we have an 800x1200 image, posterised into four colours (indices 0 to 3).
Using the .getcolors()
method, we can see how many pixels correspond to each colour (rather than an account of the colours present in the image, as might otherwise be expected):
In [11]:
img_poster.getcolors()
Out[11]:
And using the .getpalette()
method, we can obtain the four RGB tuple values from the image. For a $k$-colour palette, these are the first $3k$ values returned by the .getpalette()
method:
In [12]:
palette = img_poster.getpalette()[:3*k]
print(palette)
This shows that we have the colour indexing:
{0: (0, 0, 0), # black
1: (255, 255, 255), #white
2: (192, 114, 75), # red
3: (140, 167, 212) # blue
}
At this point then, we have values in four classes ${0, 1, 2, 3}$, and a well-distinguished colour associated with each class, for any datapoint in $(x, y)$ where $0 < x \leq 1280$ and $0 < y \leq 800$.
The posterised image is easy to save, though we have to remember that 'P'
(palette) images can't be saved in all raster formats, and '.convert()'
must be used:
In [13]:
img_poster.convert('RGB').save('data/img_poster.jpg')
The numpy
array can be saved separately. We do this below, in tab-separated plain text format:
In [14]:
savetxt('data/poster_array.tab', img_array, delimiter='\t')
There's no standard way to save our colour palette corresponding to the array, so we write a function to do this:
In [15]:
def save_palette(filename, palette, k):
"""Save the first k colours from the passed RGB colour palette.
Writes the first k RGB colours from the ImagePalette colour
palette to the named file. The output format is tab-separated
RGB integers, with each line as
R\tG\t\B
"""
with open(filename, 'w') as outfh:
for idx in range(k):
outfh.write('\t'.join([str(val) for val in
palette[idx * 3:idx * 3 + 3]]) + '\n')
In [16]:
save_palette('data/poster_palette.tab', palette, k)
The Image
class has a putpalette()
method ([http://pillow.readthedocs.org/en/latest/reference/Image.html]) that accepts a complete (768 member) list of RGB integer values, or a shorter list, which it then pads with zeros.
In [26]:
img_poster.putpalette([255, 255, 255, 192, 114, 75, 140, 167, 212, 0, 0, 0])
In [27]:
img_poster
Out[27]:
In [ ]: