13

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.

1
  • I don't know of an off-the-shelf way to do this, but my suggestion would be to find some images of ascii characters, use scikit-image to convert them to binary (thresholding) and then your will have your numpy array automatically. Commented Aug 29, 2017 at 20:13

2 Answers 2

10

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))

enter image description here enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Works, just a small typo: the first test letter should be plt.imshow(text_phantom('A', 100)) since size is not an array.
This doesn't really answer the question. This shows how to write to a canvas and then convert the canvas to an array. But how do I write into an existing array? I guess i could use numpy.where to copy the text off the canvas. But the anti-alias will not work in that case
7

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

  1. text - Python string, it can be one letter or many letters or even multiline (with '\n' letter inside to split lines).
  2. 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 path c:/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 code PIL.ImageFont.truetype(...).
  3. fontsize - size of font to be used, sizes are expressed in units specific for given font file. You can specify only one of fontsize or width/height.
  4. 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.
  5. remove_gaps - if this param is True then extra spaces around text (background) will be removed. Most of fonts have extra spaces, e.g. small letter m has more space at top, capital letter T has 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 is False (which is default) then space is kept in the amount given by Font Glyphs.
  6. 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.

Try it online!

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:

enter image description here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.