I would like to ask if there an easy an efficient way to render a given character to a numpy array. What I would like is a function that accepts a character as input, and returns a numpy array which then I can use as an argument of plt.imshow() function. Cant really find that on the internet, apart from a couple of solutions that require a lot of dependancies, when it seems like an easy task.
2 Answers
ODL has text_phantom which does exactly this with some bells and whistles.
To give you a simplified implementation, you can use the PIL library. Specifically you need to decide on the image size and font size, then it is rather straightforward.
from PIL import Image, ImageDraw, ImageFont
import numpy as np
def text_phantom(text, size):
# Availability is platform dependent
font = 'arial'
# Create font
pil_font = ImageFont.truetype(font + ".ttf", size=size // len(text),
encoding="unic")
text_width, text_height = pil_font.getsize(text)
# create a blank canvas with extra space between lines
canvas = Image.new('RGB', [size, size], (255, 255, 255))
# draw the text onto the canvas
draw = ImageDraw.Draw(canvas)
offset = ((size - text_width) // 2,
(size - text_height) // 2)
white = "#000000"
draw.text(offset, text, font=pil_font, fill=white)
# Convert the canvas into an array with values in [0, 1]
return (255 - np.asarray(canvas)) / 255.0
This gives, for example:
import matplotlib.pyplot as plt
plt.imshow(text_phantom('A', 100))
plt.imshow(text_phantom('Longer text', 100))
2 Comments
plt.imshow(text_phantom('A', 100)) since size is not an array.numpy.where to copy the text off the canvas. But the anti-alias will not work in that caseI implemented my own drawing function, called text_draw_np(...), it draws any text (not just single letter but even multiline is allowed) to numpy RGB array using PIL library. It supports coloring and stretching (changing aspect ratio) of text, also optional gaps (whitespace around text) removing functionality.
To use next code install one time pip modules python -m pip install pillow numpy matplotlib, here matplotlib is not required for drawing function itself, it is only used for tests.
Examples of usage/tests see at the end of code, right after my function. After code there is also example of drawing different letters/texts in different colors, fontsize, width/height and stretching.
Simplest use-case of drawing one or several letters is this result_numpy_array = text_draw_np('ABC', height = 200), here height of resulting image is 200 pixels.
Function accepts next params:
text- Python string, it can be one letter or many letters or even multiline (with'\n'letter inside to split lines).font- file name or path (string) to the file containing font. Font can be any TrueType font, by default I used default Windows font Arial from pathc:/windows/fonts/arial.ttf, on Linux/MacOS you should correct this path, also you may want to download some free TrueType font from internet, e.g. from here. Also other formats may be supported, all supported by FreeType library, but need single-line modification of codePIL.ImageFont.truetype(...).fontsize- size of font to be used, sizes are expressed in units specific for given font file. You can specify only one offontsizeorwidth/height.width/height- specified in pixels, if you specify both then drawn text will be stretched to fill given width/height. If you specify just any one of those (another is None) then the other will be computed automatically to keep aspect ratio, and text will be un-stretched.remove_gaps- if this param isTruethen extra spaces around text (background) will be removed. Most of fonts have extra spaces, e.g. small lettermhas more space at top, capital letterThas less space. This space is needed so that all letters have same height, also space is needed for multi-line text to have some space between lines. If remove_gaps isFalse(which is default) then space is kept in the amount given by Font Glyphs.color- foreground color (default is'black'),bg- background color (default is'white'), both can be either common color string name like'red'/'green'/'blue', or RGB tuple like(0, 255, 0)for green.
def text_draw_np(text, *, width = None, height = None, fontsize = None, font = 'c:/windows/fonts/arial.ttf', bg = (255, 255, 255), color = (0, 0, 0), remove_gaps = False, cache = {}):
import math, numpy as np, PIL.Image, PIL.ImageDraw, PIL.ImageFont, PIL.ImageColor
def get_font(fname, size):
key = ('font', fname, size)
if key not in cache:
cache[key] = PIL.ImageFont.truetype(fname, size = size, encoding = 'unic')
return cache[key]
def text_size(text, font):
if 'tsd' not in cache:
cache['tsi'] = PIL.Image.new('RGB', (1, 1))
cache['tsd'] = PIL.ImageDraw.Draw(cache['tsi'])
return cache['tsd'].textsize(text, font)
if fontsize is not None:
pil_font = get_font(font, fontsize)
text_width, text_height = text_size(text, pil_font)
width, height = text_width, text_height
else:
pil_font = get_font(font, 24)
text_width, text_height = text_size(text, pil_font)
assert width is not None or height is not None, (width, height)
width, height = math.ceil(width) if width is not None else None, math.ceil(height) if height is not None else None
pil_font = get_font(font, math.ceil(1.2 * 24 * max(
([width / text_width] if width is not None else []) +
([height / text_height] if height is not None else [])
)))
text_width, text_height = text_size(text, pil_font)
if width is None:
width = math.ceil(height * text_width / text_height)
if height is None:
height = math.ceil(width * text_height / text_width)
canvas = PIL.Image.new('RGB', (text_width, text_height), bg)
draw = PIL.ImageDraw.Draw(canvas)
draw.text((0, 0), text, font = pil_font, fill = color)
if remove_gaps:
a = np.asarray(canvas)
bg_rgb = PIL.ImageColor.getrgb(bg)
b = np.zeros_like(a)
b[:, :, 0] = bg_rgb[0]; b[:, :, 1] = bg_rgb[1]; b[:, :, 2] = bg_rgb[2]
t0 = np.any((a != b).reshape(a.shape[0], -1), axis = -1)
top, bot = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
t0 = np.any((a != b).transpose(1, 0, 2).reshape(a.shape[1], -1), axis = -1)
lef, rig = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
a = a[top : bot, lef : rig]
canvas = PIL.Image.fromarray(a)
canvas = canvas.resize((width, height), PIL.Image.LANCZOS)
return np.asarray(canvas)
import matplotlib.pyplot as plt, matplotlib
fig, axs = plt.subplots(3, 3, constrained_layout = True)
axs[0, 0].imshow(text_draw_np('A', height = 500), interpolation = 'lanczos')
axs[0, 1].imshow(text_draw_np('B', height = 500, color = 'white', bg = 'black'), interpolation = 'lanczos')
axs[0, 2].imshow(text_draw_np('0 Stretch,No-Gaps!', width = 500, height = 500, color = 'green', bg = 'magenta', remove_gaps = True), interpolation = 'lanczos')
axs[1, 0].imshow(text_draw_np('1 Stretch,No-Gaps!', width = 1500, height = 100, color = 'blue', bg = 'yellow', remove_gaps = True), interpolation = 'lanczos')
axs[1, 1].imshow(text_draw_np('2 Stretch,With-Gaps', width = 500, height = 200, color = 'red', bg = 'gray'), interpolation = 'lanczos')
axs[1, 2].imshow(text_draw_np('3 By-Height-300', height = 300, color = 'black', bg = 'lightgray'), interpolation = 'lanczos')
axs[2, 0].imshow(text_draw_np('4 By-FontSize-40', fontsize = 40, color = 'purple', bg = 'lightblue'), interpolation = 'lanczos')
axs[2, 1].imshow(text_draw_np(''.join([(chr(i) + ('' if (j + 1) % 7 != 0 else '\n')) for j, i in enumerate(range(ord('A'), ord('Z') + 1))]),
fontsize = 40, font = 'c:/windows/fonts/cour.ttf'), interpolation = 'lanczos')
axs[2, 2].imshow(text_draw_np(''.join([(chr(i) + ('' if (j + 1) % 16 != 0 else '\n')) for j, i in enumerate(range(32, 128))]),
fontsize = 40, font = 'c:/windows/fonts/cour.ttf'), interpolation = 'lanczos')
#plt.tight_layout(pad = 0.05, w_pad = 0.05, h_pad = 0.05)
plt.show()
Output:



scikit-imageto convert them to binary (thresholding) and then your will have your numpy array automatically.