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:

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

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