0

I'm trying to do an image outline in PIL. My goal is to have text with some extra lines that will be visible on any background (including images, not just solid colours), kinda like outlined movie subtitles.

There are many answers here and texts on the web, but none of them work for me. They boil down to:

  1. (usually for text) Do for-loops to put text to positions (x+dx, y+dy) for dx, dx in range(-radius, radius + 1).

  2. Blur the image.

  3. Make a contour and draw it with a thick line.

  4. Edge-detect algorithms.

I tried these, but the quality of the results was bad. This is for a project that really needs to look professional.

If I was doing this in Gimp, I would likely select the transparent part of the image with "Select by Color" tool, then invert the selection (to select everything that's not transparent), then grow the selection (giving me nicely rounded shape around corners), then feather it a bit (to get smoother lines), and then paint it with a solid colour, on a layer below my image.

Is it possible to do something like this in PIL, or anything compatible (basically, anything that can take NumPy array "images")?

HansHirse
  • 18,010
  • 10
  • 38
  • 67
Vedran Šego
  • 3,553
  • 3
  • 27
  • 40
  • Relevant [writing-a-gimp-python-script](https://stackoverflow.com/questions/12662676/writing-a-gimp-python-script) – stovfl Nov 18 '19 at 21:54

1 Answers1

1

Unfortunately, you haven't shown any of your trials, so that one could've seen, what your results look like to get an impression, what you consider "bad". So, as you mention images stored as NumPy arrays, OpenCV might be an option here.

I will follow a combination of the mentioned ideas:

  • Generate an empty text plane with the same dimensions as the image, and add an additional alpha channel set to 0 (not visible).
  • Put the text outline: Desired background color (let's say yellow), large thickness.
  • Blur the whole text plane heavily, including the alpha channel. So, you get your feathered outline.
  • Put the actual text: Desired foreground color (let's say black), normal thickness.
  • Blur the whole text plane slightly, just to smooth the generated text. (Beautiful text is not one of OpenCV's strengths!)
  • Generate output by linear combination of image and text plane using the plane's alpha channel.

That'd be the code:

import cv2
import numpy as np

# Open image, Attention: OpenCV uses BGR ordering by default!
image = cv2.imread('path/your/image.png', cv2.IMREAD_COLOR)

# Set up text properties
loc = (250, 500)
text = 'You were the chosen one!'
c_fg = (0, 255, 255, 255)
c_bg = (0, 0, 0, 255)

# Initialize overlay text plane
overlay = np.zeros((image.shape[0], image.shape[1], 4), np.uint8)

# Put text outline, larger thickness, color of outline (here: black)
cv2.putText(overlay, text, loc, cv2.FONT_HERSHEY_COMPLEX, 1.0, c_bg, 9, cv2.LINE_AA)

# Blur text plane (including alpha channel): Heavy blur
overlay = cv2.GaussianBlur(overlay, (21, 21), sigmaX=10, sigmaY=10)

# Put text, normal thickness, color of overlay (here: yellow)
cv2.putText(overlay, text, loc, cv2.FONT_HERSHEY_COMPLEX, 1.0, c_fg, 2, cv2.LINE_AA)

# Blur text plane (inclusing alpha channel): Very slight blur
overlay = cv2.GaussianBlur(overlay, (3, 3), sigmaX=0.5, sigmaY=0.5)

# Add overlay text plane to image (channel by channel)
output = np.zeros(image.shape, np.uint8)
for i in np.arange(3):
    output[:, :, i] = image[:, :, i] * ((255 - overlay[:, :, 3]) / 255) + overlay[:, :, i] * (overlay[:, :, 3] / 255)

cv2.imshow('output', output)
cv2.waitKey(0)
cv2.destroyAllWindows()

The parameters of the blurring are manually set. Different images and text sizes will require further adaptations.

Here's an example output:

Output 1

Even using a foreground color similar to the image' background, the text is still readable - at least from my point of view:

Output 2

So, now the big question: Is that result considered "bad"?

Hope that helps!

HansHirse
  • 18,010
  • 10
  • 38
  • 67
  • Thank you. `imshow` doesn't work for me on Fedora, so I replaced it with `imwrite`. I used a random PNG from my disk and the rest of the code is yours, unchanged. The result is this: https://i.imgur.com/1szgTVi.png As you can see, the blurred background has a sharp, pixelised edge, and the foreground colour is not there. Any idea why that is? It's OpenCV 3.2.0.7 under Python 2 (not my choice, and I cannot move to Python 3 at the moment). – Vedran Šego Nov 19 '19 at 11:12
  • Upgrading OpenCV to 4.1.1.26 didn't help. – Vedran Šego Nov 19 '19 at 11:16
  • 1
    @VedranŠego A general remark: Please describe the environment you use in your question to prevent such issues. I likely wouldn't have answered, if I had known that you use Python 2. One idea, after shortly reading about Python 2 / 3 differences: Integer divisions in Python 2 seem to always return integers. So, maybe try explicit casts to float for the calculations inside the loop. Otherwise, I'm afraid, I can't further help you. – HansHirse Nov 19 '19 at 11:22
  • Well spotted! Thank you, and sorry for the omission. Python 2/3 differences shouldn't really kick in for things like this, apart from the division, which I completely forgot about. It works now, and I'll try to adapt it to my project. – Vedran Šego Nov 19 '19 at 11:43