Search code examples
pythonimagetransparencypython-imaging-librarycolorize

Colorize image while preserving transparency with PIL?


Okay, here's the situation:

I want to use the Python Image Library to "theme" an image like this:

Theme color: swatch showing tint color "#33B5E5"

IN: http://mupload.nl/img/olpiyj9is.png OUT: http://mupload.nl/img/fiaoq6gk5.png

I got the result using this commands with ImageMagick:

convert image.png -colorspace gray image.png
mogrify -fill "#33b5e5" -tint 100 image.png
Explanation:


The image is first converted to black-and-white, and then it is themed.

I want to get the same result with the Python Image Library. But it seems I'm having some problems using it since:

  1. Can not handle transparency
  2. Background (transparency in main image) gets themed too..

I'm trying to use this script:

import Image
import ImageEnhance

def image_overlay(src, color="#FFFFFF", alpha=0.5):
    overlay = Image.new(src.mode, src.size, color)
    bw_src = ImageEnhance.Color(src).enhance(0.0)
    return Image.blend(bw_src, overlay, alpha)

img = Image.open("image.png")
image_overlay(img, "#33b5e5", 0.5)

You can see I did not convert it to a grayscale first, because that didn't work with transparency either.

I'm sorry to post so many issues in one question, but I couldn't do anything else :$

Hope you all understand.


Solution

  • Note: There's a Python 3/pillow fork of PIL version of this answer here.

    Update 4: Guess the previous update to my answer wasn't the last one after all. Although converting it to use PIL exclusively was a major improvement, there were a couple of things that seemed like there ought to be better, less awkward, ways to do, if only PIL had the ability.

    Well, after reading the documentation closely as well as some of the source code, I realized what I wanted to do was in fact possible. The trade-off was that now it has to build the look-up table used manually, so the overall code is slightly longer. However the result is that it only needs to make one call to the relatively slow Image.point() method, instead of three of them.

    from PIL import Image
    from PIL.ImageColor import getcolor, getrgb
    from PIL.ImageOps import grayscale
    
    def image_tint(src, tint='#ffffff'):
        if Image.isStringType(src):  # file path?
            src = Image.open(src)
        if src.mode not in ['RGB', 'RGBA']:
            raise TypeError('Unsupported source image mode: {}'.format(src.mode))
        src.load()
    
        tr, tg, tb = getrgb(tint)
        tl = getcolor(tint, "L")  # tint color's overall luminosity
        if not tl: tl = 1  # avoid division by zero
        tl = float(tl)  # compute luminosity preserving tint factors
        sr, sg, sb = map(lambda tv: tv/tl, (tr, tg, tb))  # per component adjustments
    
        # create look-up tables to map luminosity to adjusted tint
        # (using floating-point math only to compute table)
        luts = (map(lambda lr: int(lr*sr + 0.5), range(256)) +
                map(lambda lg: int(lg*sg + 0.5), range(256)) +
                map(lambda lb: int(lb*sb + 0.5), range(256)))
        l = grayscale(src)  # 8-bit luminosity version of whole image
        if Image.getmodebands(src.mode) < 4:
            merge_args = (src.mode, (l, l, l))  # for RGB verion of grayscale
        else:  # include copy of src image's alpha layer
            a = Image.new("L", src.size)
            a.putdata(src.getdata(3))
            merge_args = (src.mode, (l, l, l, a))  # for RGBA verion of grayscale
            luts += range(256)  # for 1:1 mapping of copied alpha values
    
        return Image.merge(*merge_args).point(luts)
    
    if __name__ == '__main__':
        import os
    
        input_image_path = 'image1.png'
        print 'tinting "{}"'.format(input_image_path)
    
        root, ext = os.path.splitext(input_image_path)
        result_image_path = root+'_result'+ext
    
        print 'creating "{}"'.format(result_image_path)
        result = image_tint(input_image_path, '#33b5e5')
        if os.path.exists(result_image_path):  # delete any previous result file
            os.remove(result_image_path)
        result.save(result_image_path)  # file name's extension determines format
    
        print 'done'
    

    Here's a screenshot showing input images on the left with corresponding outputs on the right. The upper row is for one with an alpha layer and the lower is a similar one that doesn't have one.

    sample input and output images showing results of image with and without alpha