I am using the Python Imaging Library to colorize a black and white image with a lookup table that defines the color relationships. The lookup table is simply a 256-element list of RGB tuples:
>>> len(colors)
256
>>> colors[0]
(255, 237, 237)
>>> colors[127]
(50, 196, 33)
>>>
My first version used the getpixel()
and putpixel()
methods:
for x in range(w):
for y in range(h):
pix = img.getpixel((x,y))
img.putpixel((x,y), colors[pix[0]])
This was horribly slow. A profile
report pointed to the putpixel
and getpixel
methods as the culprits. A little investigation (i.e, read the docs) and I find "Note that this method is relatively slow." re: putpixel
. (actual runtime: 53s in putpixel
and 50s getpixel
for a 1024x1024 image)
Based on the suggestion in the docs, I used im.load()
and direct pixel access instead:
pixels = img.load()
for x in range(w):
for y in range(h):
pix = pixels[x, y]
pixels[x, y] = colors[pix[0]]
Processing sped up by an order of magnitude, but is still slow: about 3.5s to process a 1024x1024 image.
A more thorough study of the PIL docs seems to indicate Image.point()
is exactly intended for this purpose:
im.point(table)
=> image
im.point(function)
=> imageReturns a copy of the image where each pixel has been mapped through the given table. The table should contains 256 values per band in the image. If a function is used instead, it should take a single argument. The function is called once for each possible pixel value, and the resulting table is applied to all bands of the image.
I've spent some time hacking around with the interface, but can't quite seem to get it right. Forgive my ignorance, but PIL's docs are curt and I don't have much image processing experience. I've googled around a bit and turned up a few examples, but nothing that made the usage "click" for me. Thus, finally, my questions:
Image.point()
the right tool for this job?Image.point()
expect the table?With PIL you can easily access and change the data stored in the pixels of an image. To get the pixel map, call load() on an image. The pixel data can then be retrieved by indexing the pixel map as an array.
To resize an image, you call the resize() method on it, passing in a two-integer tuple argument representing the width and height of the resized image. The function doesn't modify the used image; it instead returns another Image with the new dimensions.
Modes. The mode of an image is a string which defines the type and depth of a pixel in the image. Each pixel uses the full range of the bit depth.
Is Image.point() the right tool for this job?
Yes indeed, Image.point()
is perfect for this job
What format/structure does Image.point() expect the table?
You should flatten the list so instead of [(12, 140, 10), (10, 100, 200), ...]
use:
[12, 140, 10, 10, 100, 200, ...]
Here is a quick example I just tried:
im = im.point(range(256, 0, -1) * 3)
And by the way, if you need more control over colors and you feel Image.point is not for you you can also use Image.getdata
and Image.putdata
to change colors more quickly than both load
and putpixel
. It is slower than Image.point
though.
Image.getdata
gives you the list of all pixels, modify them and write them back using Image.putdata
. It is that simple. But try to do it using Image.point
first.
EDIT
I made a mistake in the first explanation, I'll explain correctly now:
The color table actually is like this
[0, 1, 2, 3, 4, 5, ...255, 0, 1, 2, 3, ....255, 0, 1, 2, 3, ...255]
Each band range next to the other. To change the color (0, 0, 0) to (10, 100, 10) it need to become like this:
[10, 1, 2, 3, 4, 5, ...255, 100, 1, 2, 3, ....255, 10, 1, 2, 3, ...255]
To transform your color list into the right format try this:
table = sum(zip(*colors), ())
I think my first example should demonstrate the formate for you.
I think it might be more typical to point
on a band-by-band basis like so (lifted directly from the PIL tutorial):
# split the image into individual bands
source = im.split()
R, G, B = 0, 1, 2
# select regions where red is less than 100
mask = source[R].point(lambda i: i < 100 and 255)
# process the green band
out = source[G].point(lambda i: i * 0.7)
# paste the processed band back, but only where red was < 100
source[G].paste(out, None, mask)
# build a new multiband image
im = Image.merge(im.mode, source)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With