Colorize image while preserving transparency with PIL?
Asked Answered
S

2

6

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: https://static.mcmap.net/file/mcmap/ZG-Ab5ovKR00bFYvWV3Qame/img/olpiyj9is.png OUT: https://static.mcmap.net/file/mcmap/ZG-Ab5ovKR00bFYvWV3Qame/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.

Servia answered 3/9, 2012 at 17:19 Comment(13)
Try using Image.composite() instead of Image.blend().Armillia
composite(image1, image2, mask), what to use as mask??Servia
Can you show a concrete example of the output you desire from the inputs (just the inputs and outputs for one or more pixels numerically including alpha, not a graphic)? I think I understand the transparency issue but not what you mean by "theme an image" from one color to another.Armillia
Sure, I can do this with ImageMagick: IN: mupload.nl/img/olpiyj9is.png OUT: mupload.nl/img/fiaoq6gk5.pngServia
Sorry, while useful overall to show what you want to happen, those two images (as well as the two in your question) aren't enough to determine precisely what went on at the pixel level between them -- which needs to be described to figure out if something equivalent is possible with the PIL. Perhaps if you updated your question and provided detailed information about what you did in ImageMagick...Armillia
Updated the question, updated images (but couldn't post on the forum because I'm a new member). I hope it's more clear now and you understand it...Servia
Your update looks helpful although the description of what -tint does is a little vague in ImageMagick's documentation, especially with respect to non-grey "mid-range colors". That may not matter since you say with it you first convert the image to B&W. so I'll see if I can come up with something.Armillia
Question: Why don't you just use PythonMagick since it apparently does exactly what you want to do?Armillia
Because my tool is meant to be cross-platform (Including Mac OS X), and 32-bits Mac OS X gives conflicts with ImageMagick...Servia
Hmmm, have you tried their Mac OS X Binary Release? Are you trying to build a commercial app?Armillia
Yeah, I did.. Also tried building it from the source, but I got errors over and over again, also related to XCode not installed (For wich Mac OS X Lion is necessary at the moment)... So no..Servia
I'm sorry I didn't see this question earlier, I might have been able to help. One thing I don't understand, how did ImageMagick know which part of the image needed to be colorized and which part didn't? The -colorspace gray should have removed all the original color information.Gebelein
Imagemagick is only binary files, I haven't been able to see the source...Servia
A
9

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

Armillia answered 7/9, 2012 at 2:10 Comment(14)
Hey :) I'll try this out when I get home.. but I need to theme other images with it too, so the luminosity is not the same every time... thanks in advance, I'll try this out :)Servia
Despite what it may look like to you, there's nothing in it hardcoded for any particular luminosity. The constants used are part of the standard NTSC formula for converting from RGB to Greyscale.Armillia
Confirmed working from every color to every color. Thanks a ton!! Ciao!Servia
One more thing: what if the image does NOT have alpha in it. It will give an error: "ValueError: need more than 3 values to unpack".Servia
Sorry about the error for non-alpha images, I warned you that it was a little crude and made assumptions about the source image -- one of them was that it has alpha. Since that doesn't enter into any calculations fixing it would be easy. The tricky part might be making it handle both elegantly without to much overhead. If you think my answer is any good, please up-vote it, too, as is customary.Armillia
Ofcourse it is, your answer is great. I immediately clicked the ^ icon, but I need 15 reputation, while I have just 3 :( I entered StackOverflow just to ask this question. I will vote you up as soon as I can!Servia
OK, sure, I understand...only mentioned it because many people new to SO don't realize that accepting an answer is separate from up-voting it. Anyway, I'm working on making it handle RGB as well as RGBA images, and optimizing things a bit. Didn't want to bother until you had a chance to bang on it a little and confirm whether it seemed to produce the desired results.Armillia
wokring PERFECT. Thanks a ton :) Because I am not experienced enough to answer questions before others do, I can not earn reputation that fast, but I WILL vote you up! Thanks :DServia
16 reputation, the first thing I did was voting this up as promised :) thanks again!Servia
@mDroidd: Been a while, but thanks for remembering...and congratulations on the growing rep! As an exercise, you should pick-up where I left off and try enhancing it to also handle "L" (and "LA") greyscale images. ;-)Armillia
Haha, almost sure I won't be able, but I'll certainly try ;) BTW, because you seem to know pretty much about PIL, do you know a way to solve this? https://mcmap.net/q/681292/-pil-image-mode-quot-p-quot-gt-quot-rgba-quot/… ThanksServia
@mDroidd: Although it might get a little tricky because of my optimizations, making it handle grayscale input images should be relatively easy -- because the temporary luminosity l image is the input image in those cases and doesn't need to be calculated internally on-the-fly. I'll take a look at the other question and see if I can come up with something.Armillia
New to python. Getting this error: luts = ((map(lambda lr: int(lr*sr + 0.5), range(256))) + TypeError: unsupported operand type(s) for +: 'map' and 'map' @ArmilliaBenempt
@NaitikSoni: Python 2 vs 3 issue. You will need to explicitly convert each map(…) call to list(map(…)).Armillia
R
6

You need to convert to grayscale first. What I did:

  1. get original alpha layer using Image.split()
  2. convert to grayscale
  3. colorize using ImageOps.colorize
  4. put back original alpha layer

Resulting code:

import Image
import ImageOps

def tint_image(src, color="#FFFFFF"):
    src.load()
    r, g, b, alpha = src.split()
    gray = ImageOps.grayscale(src)
    result = ImageOps.colorize(gray, (0, 0, 0, 0), color) 
    result.putalpha(alpha)
    return result

img = Image.open("image.png")
tinted = tint_image(img, "#33b5e5")
Rightness answered 6/9, 2012 at 10:13 Comment(5)
The alpha put-back is working thanks :) BUT, after using your script 2 times, the image has become totally black... Any suggestions? If it is working afterwards I will give you the "Working solution" checkbox :)Servia
I wanted to add that I think the grayscale is the problem.. It's getting darker every time... Thanks anywayServia
You can use gray = ImageOps.autocontrast(gray) to adjust brightness after grayscale.. I've noticed darkening too with the android picture you've attached, and that is because there is area with really bright (rgb part) pixels which have transparent alpha at the bottom of the image. This makes colorize use the brightest color there and as it is "masked" by alpha, everything else darkens.. I'll fix it..Rightness
Although this was not my final solution, I want to thank you for yours :)Servia
I think you're mistaken about why colorize() didn't quite work, because I had similar results using multiply(), which doesn't do any masking, on the grayscale image. The solution, I've read, is to preserve luminosity by scaling the results of colorization by the ratio of the luminosity (apparent brightness) of old color, and the new one to compensate for the apparent darkening which occurs otherwise. Since that is fairly expensive to do on a pixel-by-pixel basis, you can get by just using the ratio of the luminosity of white to that of the tint color which is the same thing.Armillia

© 2022 - 2024 — McMap. All rights reserved.