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: 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.

martineau
  • 119,623
  • 25
  • 170
  • 301
mDroidd
  • 1,213
  • 4
  • 17
  • 25
  • Try using `Image.composite()` instead of `Image.blend()`. – martineau Sep 03 '12 at 20:51
  • composite(image1, image2, mask), what to use as mask?? – mDroidd Sep 04 '12 at 13:15
  • 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. – martineau Sep 04 '12 at 16:44
  • Sure, I can do this with ImageMagick: IN: http://www.mupload.nl/img/olpiyj9is.png OUT: http://www.mupload.nl/img/fiaoq6gk5.png – mDroidd Sep 05 '12 at 14:24
  • 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... – martineau Sep 05 '12 at 16:06
  • 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... – mDroidd Sep 05 '12 at 16:52
  • 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. – martineau Sep 05 '12 at 20:08
  • Question: Why don't you just use [PythonMagick](http://www.imagemagick.org/script/api.php#python) since it apparently does exactly what you want to do? – martineau Sep 05 '12 at 20:39
  • Because my tool is meant to be cross-platform (Including Mac OS X), and 32-bits Mac OS X gives conflicts with ImageMagick... – mDroidd Sep 06 '12 at 15:57
  • Hmmm, have you tried their [Mac OS X Binary Release](http://www.imagemagick.org/script/binary-releases.php#macosx)? Are you trying to build a commercial app? – martineau Sep 06 '12 at 17:04
  • 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.. – mDroidd Sep 06 '12 at 18:14
  • 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. – Mark Ransom Sep 10 '12 at 22:00
  • Imagemagick is only binary files, I haven't been able to see the source... – mDroidd Sep 11 '12 at 05:54

2 Answers2

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

martineau
  • 119,623
  • 25
  • 170
  • 301
  • 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 :) – mDroidd Sep 07 '12 at 06:12
  • 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. – martineau Sep 07 '12 at 08:23
  • Confirmed working from every color to every color. Thanks a ton!! Ciao! – mDroidd Sep 07 '12 at 12:05
  • 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". – mDroidd Sep 07 '12 at 12:22
  • 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. – martineau Sep 07 '12 at 13:48
  • 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! – mDroidd Sep 07 '12 at 14:25
  • 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. – martineau Sep 07 '12 at 16:09
  • 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 :D – mDroidd Sep 08 '12 at 08:54
  • 16 reputation, the first thing I did was voting this up as promised :) thanks again! – mDroidd Sep 17 '12 at 16:39
  • @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. ;-) – martineau Sep 18 '12 at 01:25
  • 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? http://stackoverflow.com/questions/12462548/pil-image-mode-p-rgba/12463716#comment16768322_12463716 Thanks – mDroidd Sep 18 '12 at 14:27
  • @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. – martineau Sep 18 '12 at 17:13
  • 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' @martineau – Naitik Soni Nov 10 '20 at 07:36
  • 1
    @NaitikSoni: Python 2 vs 3 issue. You will need to explicitly convert each `map(…)` call to `list(map(…))`. – martineau Nov 10 '20 at 07:46
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")
Jan Spurny
  • 5,219
  • 1
  • 33
  • 47
  • 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 :) – mDroidd Sep 06 '12 at 15:54
  • I wanted to add that I think the grayscale is the problem.. It's getting darker every time... Thanks anyway – mDroidd Sep 06 '12 at 15:56
  • 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.. – Jan Spurny Sep 06 '12 at 16:29
  • Although this was not my final solution, I want to thank you for yours :) – mDroidd Sep 07 '12 at 12:22
  • 2
    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. – martineau Sep 10 '12 at 22:16