7

I'm watermarking some pictures using PIL and I'm having a hard time reading some of the text (black text on dark background). I can't just change text color since I've got a wide array of background colors. Is there any way to add a halo effect around the text?

For example: https://i.stack.imgur.com/zhRe1.jpg The bottom text is what I've got, and the top text is what I'm hoping to get (colors aside). I really just need a thin outline around the text. Any ideas? I can upload some code if you really think it'll make a difference, but it's just a normal PIL ImageDraw.Draw command. Thanks!

user1607225
  • 71
  • 1
  • 3

1 Answers1

14

If you don't care about speed too much, you can do it using composition:

  1. draw text with halo color on a blank RGBA image
  2. blur it
  3. draw it again with text color
  4. invert this image to get composition mask
  5. "merge" with original image

For example:

import sys
import Image, ImageChops, ImageDraw, ImageFont, ImageFilter

def draw_text_with_halo(img, position, text, font, col, halo_col):
    halo = Image.new('RGBA', img.size, (0, 0, 0, 0))
    ImageDraw.Draw(halo).text(position, text, font = font, fill = halo_col)
    blurred_halo = halo.filter(ImageFilter.BLUR)
    ImageDraw.Draw(blurred_halo).text(position, text, font = font, fill = col)
    return Image.composite(img, blurred_halo, ImageChops.invert(blurred_halo))

if __name__ == '__main__':
    i = Image.open(sys.argv[1])
    font = ImageFont.load_default()
    txt = 'Example 1234'
    text_col = (0, 255, 0) # bright green
    halo_col = (0, 0, 0)   # black
    i2 = draw_text_with_halo(i, (20, 20), txt, font, text_col, halo_col)
    i2.save('halo.png')

It has many advantages:

  • the result is smooth and looks nice
  • you can choose different filter instead of BLUR to get different "halo"
  • it works even with very large fonts and still looks great

To get thicker halo, you may use filter like this:

kernel = [
    0, 1, 2, 1, 0,
    1, 2, 4, 2, 1,
    2, 4, 8, 4, 1,
    1, 2, 4, 2, 1,
    0, 1, 2, 1, 0]
kernelsum = sum(kernel)
myfilter = ImageFilter.Kernel((5, 5), kernel, scale = 0.1 * sum(kernel))
blurred_halo = halo.filter(myfilter)

The part scale = 0.1 * sum(kernel) makes the halo thicker (small values) or dimmer (big values).

Jan Spurny
  • 5,219
  • 1
  • 33
  • 47
  • I think a proper outline would look better in this case, as in the example in the question, but this is the correct way to generate a "halo". The only downside is that the wider you make the halo the dimmer it gets. I'm a bit disappointed that PIL doesn't give a more straight-forward way of blending an RGBA image over another. – Mark Ransom Aug 17 '12 at 18:31
  • Cool thanks. Just one question... the blur is very blurred out... with black text and white blur... the blur does in fact turn out invisible on a black background. How can we sharpen this? Maybe that is what you mean by thicker halo? – Jonny Jul 07 '15 at 03:48
  • @Jonny yes, that would probably be it. Or, you can use different filter (see: http://pillow.readthedocs.org/en/latest/reference/ImageFilter.html), play with *scale* or *offset* values in http://pillow.readthedocs.org/en/latest/reference/ImageFilter.html#PIL.ImageFilter.Kernel – Jan Spurny Jul 07 '15 at 12:36