Draw text on an angle (rotated) in Python - python

I am drawing text onto a numpy array image in Python (using a custom font). Currently I am converting the image to PIL, drawing the text and then converting back to a numpy array.
import numpy as np
import cv2
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
char_image = np.zeros((200, 300, 3), np.uint8)
# convert to pillow image
pillowImage = Image.fromarray(char_image)
draw = ImageDraw.Draw(pillowImage)
# add chars to image
font = ImageFont.truetype("arial.ttf", 32)
draw.text((50, 50), 'ABC', (255, 255, 255), font=font)
# convert back to numpy array
char_image = np.array(pillowImage, np.uint8)
# show image on screen
cv2.imshow('myImage', char_image)
cv2.waitKey(0)
Is there anyway to draw the text on a given angle, ie. 33 degrees?
Rotating the image once the text has been drawn is not an option

You can use PIL to draw rotated text. I suggest drawing the text onto a blank image, rotating that image, and then pasting the rotated image into the main image. Something like:
Code:
def draw_rotated_text(image, angle, xy, text, fill, *args, **kwargs):
""" Draw text at an angle into an image, takes the same arguments
as Image.text() except for:
:param image: Image to write text into
:param angle: Angle to write text at
"""
# get the size of our image
width, height = image.size
max_dim = max(width, height)
# build a transparency mask large enough to hold the text
mask_size = (max_dim * 2, max_dim * 2)
mask = Image.new('L', mask_size, 0)
# add text to mask
draw = ImageDraw.Draw(mask)
draw.text((max_dim, max_dim), text, 255, *args, **kwargs)
if angle % 90 == 0:
# rotate by multiple of 90 deg is easier
rotated_mask = mask.rotate(angle)
else:
# rotate an an enlarged mask to minimize jaggies
bigger_mask = mask.resize((max_dim*8, max_dim*8),
resample=Image.BICUBIC)
rotated_mask = bigger_mask.rotate(angle).resize(
mask_size, resample=Image.LANCZOS)
# crop the mask to match image
mask_xy = (max_dim - xy[0], max_dim - xy[1])
b_box = mask_xy + (mask_xy[0] + width, mask_xy[1] + height)
mask = rotated_mask.crop(b_box)
# paste the appropriate color, with the text transparency mask
color_image = Image.new('RGBA', image.size, fill)
image.paste(color_image, mask)
How does it work:
Create a transparency mask.
Draw the text onto the mask.
Rotate the mask, and crop to proper size.
Paste the desired color into the image, using the rotated transparency mask containing the text.
Test Code:
import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
char_image = np.zeros((100, 150, 3), np.uint8)
# convert to pillow image
pillowImage = Image.fromarray(char_image)
# draw the text
font = ImageFont.truetype("arial.ttf", 32)
draw_rotated_text(pillowImage, 35, (50, 50), 'ABC', (128, 255, 128), font=font)
pillowImage.show()
Results:

Using matplotlib, first visualize array and draw on it, get the raw data from the figure back.
Pro: both tools are quite high level and let you deal with many details of the process. ax.annotate() offers flexibility for where and how to draw and set font properties, and plt.matshow() offers flexibility that lets you deal with aspects of array visualization.
import matplotlib.pyplot as plt
import scipy as sp
# make Data array to draw in
M = sp.zeros((500,500))
dpi = 300.0
# create a frameless mpl figure
fig, axes = plt.subplots(figsize=(M.shape[0]/dpi,M.shape[1]/dpi),dpi=dpi)
axes.axis('off')
fig.subplots_adjust(bottom=0,top=1.0,left=0,right=1)
axes.matshow(M,cmap='gray')
# set custom font
import matplotlib.font_manager as fm
ttf_fname = '/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-B.ttf'
prop = fm.FontProperties(fname=ttf_fname)
# annotate something
axes.annotate('ABC',xy=(250,250),rotation=45,fontproperties=prop,color='white')
# get fig image data and read it back to numpy array
fig.canvas.draw()
w,h = fig.canvas.get_width_height()
Imvals = sp.fromstring(fig.canvas.tostring_rgb(),dtype='uint8')
ImArray = Imvals.reshape((w,h,3))

Related

How to generate an alpha image with a color range with PIL?

I have a grayscale image and I want to create an alpha layer based on a range of pixel values. I want to know how can I create a fall-off function to generate such image.
The original image is the following:
I can use the color range in photoshop to select the shadows with fuzziness of 20%
And the resultant alpha channel is the following:
With fuzziness of 100%:
How can I generate such alpha channels in python with PIL?
I thought that maybe a subtract, but it does not generates a
The code to generate the image with Numpy and PIL:
from PIL import Image
import numpy as np
img = np.arange(0,256, 0.1).astype(np.uint8)
img = np.reshape(img, (img.shape[0], 1))
img = np.repeat((img), 500, axis=1)
img = Image.fromarray(img.T)
I tried to create a fall-off function from the distance of the pixel values but it does not have the same gradient. Maybe there is a different way?
def gauss_falloff(distance, c=0.2, alpha=255):
new_value = alpha * np.exp(-1 * ((distance) ** 2) / (c**2))
new_value = new_value.clip(0,255)
return new_value.astype(np.uint8)
test = img.T / 255
test = np.abs(test - pixel)
test = gauss_falloff(test, c=0.2, alpha=255)
test = Image.fromarray(test)
With my code:
Here's how you could do that
from PIL import Image, ImageDraw
# Create a new image with a transparent background
width, height = 300, 300
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
# Create a drawing context for the image
draw = ImageDraw.Draw(image)
# Set the starting and ending colors for the gradient
start_color = (255, 0, 0)
end_color = (0, 0, 255)
# Draw a gradient line with the specified color range
for x in range(width):
color = tuple(int(start_color[i] + (end_color[i] - start_color[i]) * x / width)
for i in range(3))
draw.line((x, 0, x, height), fill=color)
# Save the image
image.save('gradient.png')
This code creates a new image with a transparent background and a drawing context for that image. Then it draws a gradient line on the image with the specified color range. Finally, it saves the image as a PNG file.
Note: The Python Imaging Library (PIL) has been replaced by the Pillow library, which is a fork of PIL. If you are using Pillow, you can use the same code as above, but you need to import the Image and ImageDraw modules from the Pillow package instead of the PIL package.

How would I use PIL to "extend" an image, and then draw a black rectangle with text on it?

Basically, what I want to do with Pillow is:
I want to get an image, and then extend the size of the image from the bottom so I'm able to fit a black rectangle with a four digit code on it. How would I do this? I tried to, but my text ended up being, for some reason, extremely small and unreadable and my rectangle wasn't perfect.
If it makes it easier, here's my image: https://i.stack.imgur.com/o9eYr.jpg
And here's what I want the end result to be: https://i.stack.imgur.com/GZ4uu.jpg (take a look at the bottom of the image)
I would suggest ImageOps.expand to expand your canvas:
#!/usr/bin/env python3
from PIL import Image, ImageDraw, ImageFont, ImageOps
# Load image
im = Image.open('o9eYr.jpg')
# Define font size, and annotation and height of padding above and below annotation
fontSize = 130
annotation = "GVVL"
padding = 20
# Load font and work out size of annotation
font = ImageFont.truetype("/System/Library/Fonts//Menlo.ttc", fontSize)
tw, th = font.getsize(annotation)
# Extend image at bottom and get height and width of new canvas
extended = ImageOps.expand(im, border=(0,0,0,th+2*padding), fill=(0,0,0))
w, h = extended.size
# Get drawing context and annotate
draw = ImageDraw.Draw(extended)
draw.text(((w-tw)//2, h-th-padding), annotation,(255,255,255),font=font)
extended.save('result.jpg')
You could create a new black image, paste the desired image and add text.
from PIL import Image, ImageFont, ImageDraw
base_img = Image.open('tmp.jpg')
base_size = base_img.size
new_size = (base_size[0], base_size[1] + 150)
img = Image.new("RGB", new_size)
img.paste(base_img, (0, 0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("microsoftsansserif.ttf", 145) # (<font-file>, <font-size>)d
draw.text((base_size[0] // 2 - 150, base_size[1]),"GVVL",(255,255,255),font=font) # (x, y),"text",(r,g,b)
img.save('out.jpg')
Result:

Render Text in an Image with Not Straight Borders

How to I render my text with borders not straight, just like the second image? (The font of the two image is different but the way the borders is rendered is of concern.)
My initial code is
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from random import seed
from random import randint
import numpy as np
import os.path
#Returns the text size in terms of width and height.
def getSize(txt, font):
testImg = Image.new('RGB', (1, 1))
testDraw = ImageDraw.Draw(testImg)
return testDraw.textsize(txt, font)
text = 'lemper'
fontname = 'arial.ttf'
fontsize= 25
font = ImageFont.truetype(fontname, fontsize)
width, height = getSize(text, font)
#Creates an image with white background of constant size.
img = Image.new('RGB', (100, 100), 'white')
d = ImageDraw.Draw( img)
d.text(get_xy_coordinates(text, font), text, fill='black', font=font)
img.save("text_images/1.png")
A simple way to achieve this effect is to use a "displacement function" (X(x, y), Y(x, y)) which moves every pixel to a nearby position. This function should be smooth, with a small amplitude and somewhat irregular. It can be obtained for example by a combination of trigonometric functions with incommensurable periods.
You will have to implement a resampling function, with bilinear interpolation for good quality.

PIL Drawing a semi-transparent square overlay on image

from PIL import Image
from PIL import ImageDraw
from io import BytesIO
from urllib.request import urlopen
url = "https://i.ytimg.com/vi/W4qijIdAPZA/maxresdefault.jpg"
file = BytesIO(urlopen(url).read())
img = Image.open(file)
img = img.convert("RGBA")
draw = ImageDraw.Draw(img, "RGBA")
draw.rectangle(((0, 00), (img.size[0], img.size[1])), fill=(0,0,0,127))
img.save('dark-cat.jpg')
This is giving me a giant black square. I want it to be a semi transparent black square with a cat. Any Ideas?
Sorry, the comment I made about it being a bug was incorrect, so...
You can do it by creating a temporary image and using Image.alpha_composite() as shown in the code below. Note that it supports semi-transparent squares other than black.
from PIL import Image, ImageDraw
from io import BytesIO
from urllib.request import urlopen
TINT_COLOR = (0, 0, 0) # Black
TRANSPARENCY = .25 # Degree of transparency, 0-100%
OPACITY = int(255 * TRANSPARENCY)
url = "https://i.ytimg.com/vi/W4qijIdAPZA/maxresdefault.jpg"
with BytesIO(urlopen(url).read()) as file:
img = Image.open(file)
img = img.convert("RGBA")
# Determine extent of the largest possible square centered on the image.
# and the image's shorter dimension.
if img.size[0] > img.size[1]:
shorter = img.size[1]
llx, lly = (img.size[0]-img.size[1]) // 2 , 0
else:
shorter = img.size[0]
llx, lly = 0, (img.size[1]-img.size[0]) // 2
# Calculate upper point + 1 because second point needs to be just outside the
# drawn rectangle when drawing rectangles.
urx, ury = llx+shorter+1, lly+shorter+1
# Make a blank image the same size as the image for the rectangle, initialized
# to a fully transparent (0% opaque) version of the tint color, then draw a
# semi-transparent version of the square on it.
overlay = Image.new('RGBA', img.size, TINT_COLOR+(0,))
draw = ImageDraw.Draw(overlay) # Create a context for drawing things on it.
draw.rectangle(((llx, lly), (urx, ury)), fill=TINT_COLOR+(OPACITY,))
# Alpha composite these two images together to obtain the desired result.
img = Image.alpha_composite(img, overlay)
img = img.convert("RGB") # Remove alpha for saving in jpg format.
img.save('dark-cat.jpg')
img.show()
Here's the result of applying it to your test image:
Given that I keep coming back to this issue whenever I want to draw a transparent rectangle with PIL, I decided to give an update.
Your code is pretty much working for me if I just change one thing: Save the image in the PNG format instead of JPEG.
So when I'm running
from io import BytesIO
from urllib.request import urlopen
from PIL import Image
from PIL import ImageDraw
url = "https://i.ytimg.com/vi/W4qijIdAPZA/maxresdefault.jpg"
file = BytesIO(urlopen(url).read())
img = Image.open(file)
draw = ImageDraw.Draw(img, "RGBA")
draw.rectangle(((280, 10), (1010, 706)), fill=(200, 100, 0, 127))
draw.rectangle(((280, 10), (1010, 706)), outline=(0, 0, 0, 127), width=3)
img.save('orange-cat.png')
I get this wonderful image:
If you just want to dim the entire image, there's a simpler way:
img = Image.eval(img, lambda x: x/2)

How do I draw text at an angle using python's PIL?

Using Python I want to be able to draw text at different angles using PIL.
For example, imagine you were drawing the number around the face of a clock. The number 3 would appear as expected whereas 12 would we drawn rotated counter-clockwise 90 degrees.
Therefore, I need to be able to draw many different strings at many different angles.
Draw text into a temporary blank image, rotate that, then paste that onto the original image. You could wrap up the steps in a function. Good luck figuring out the exact coordinates to use - my cold-fogged brain isn't up to it right now.
This demo writes yellow text on a slant over an image:
# Demo to add rotated text to an image using PIL
import Image
import ImageFont, ImageDraw, ImageOps
im=Image.open("stormy100.jpg")
f = ImageFont.load_default()
txt=Image.new('L', (500,50))
d = ImageDraw.Draw(txt)
d.text( (0, 0), "Someplace Near Boulder", font=f, fill=255)
w=txt.rotate(17.5, expand=1)
im.paste( ImageOps.colorize(w, (0,0,0), (255,255,84)), (242,60), w)
It's also usefull to know our text's size in pixels before we create an Image object. I used such code when drawing graphs. Then I got no problems e.g. with alignment of data labels (the image is exactly as big as the text).
(...)
img_main = Image.new("RGB", (200, 200))
font = ImageFont.load_default()
# Text to be rotated...
rotate_text = u'This text should be rotated.'
# Image for text to be rotated
img_txt = Image.new('L', font.getsize(rotate_text))
draw_txt = ImageDraw.Draw(img_txt)
draw_txt.text((0,0), rotate_text, font=font, fill=255)
t = img_value_axis.rotate(90, expand=1)
The rest of joining the two images together is already described on this page.
When you rotate by an "unregular" angle, you have to improve this code a little bit. It actually works for 90, 180, 270...
Here is a working version, inspired by the answer, but it works without opening or saving images.
The two images have colored background and alpha channel different from zero to show what's going on. Changing the two alpha channels from 92 to 0 will make them completely transparent.
from PIL import Image, ImageFont, ImageDraw
text = 'TEST'
font = ImageFont.truetype(r'C:\Windows\Fonts\Arial.ttf', 50)
width, height = font.getsize(text)
image1 = Image.new('RGBA', (200, 150), (0, 128, 0, 92))
draw1 = ImageDraw.Draw(image1)
draw1.text((0, 0), text=text, font=font, fill=(255, 128, 0))
image2 = Image.new('RGBA', (width, height), (0, 0, 128, 92))
draw2 = ImageDraw.Draw(image2)
draw2.text((0, 0), text=text, font=font, fill=(0, 255, 128))
image2 = image2.rotate(30, expand=1)
px, py = 10, 10
sx, sy = image2.size
image1.paste(image2, (px, py, px + sx, py + sy), image2)
image1.show()
The previous answers draw into a new image, rotate it, and draw it back into the source image. This leaves text artifacts. We don't want that.
Here is a version that instead crops the area of the source image that will be drawn onto, rotates it, draws into that, and rotates it back. This means that we draw onto the final surface immediately, without having to resort to masks.
def draw_text_90_into (text: str, into, at):
# Measure the text area
font = ImageFont.truetype (r'C:\Windows\Fonts\Arial.ttf', 16)
wi, hi = font.getsize (text)
# Copy the relevant area from the source image
img = into.crop ((at[0], at[1], at[0] + hi, at[1] + wi))
# Rotate it backwards
img = img.rotate (270, expand = 1)
# Print into the rotated area
d = ImageDraw.Draw (img)
d.text ((0, 0), text, font = font, fill = (0, 0, 0))
# Rotate it forward again
img = img.rotate (90, expand = 1)
# Insert it back into the source image
# Note that we don't need a mask
into.paste (img, at)
Supporting other angles, colors etc is trivial to add.
Here's a fuller example of watermarking diagonally. Handles arbitrary image ratios, sizes and text lengths by calculating the angle of the diagonal and font size.
from PIL import Image, ImageFont, ImageDraw
import math
# sample dimensions
pdf_width = 1000
pdf_height = 1500
#text_to_be_rotated = 'Harry Moreno'
text_to_be_rotated = 'Harry Moreno (morenoh149#gmail.com)'
message_length = len(text_to_be_rotated)
# load font (tweak ratio based on your particular font)
FONT_RATIO = 1.5
DIAGONAL_PERCENTAGE = .5
diagonal_length = int(math.sqrt((pdf_width**2) + (pdf_height**2)))
diagonal_to_use = diagonal_length * DIAGONAL_PERCENTAGE
font_size = int(diagonal_to_use / (message_length / FONT_RATIO))
font = ImageFont.truetype(r'./venv/lib/python3.7/site-packages/reportlab/fonts/Vera.ttf', font_size)
#font = ImageFont.load_default() # fallback
# target
image = Image.new('RGBA', (pdf_width, pdf_height), (0, 128, 0, 92))
# watermark
opacity = int(256 * .5)
mark_width, mark_height = font.getsize(text_to_be_rotated)
watermark = Image.new('RGBA', (mark_width, mark_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark)
draw.text((0, 0), text=text_to_be_rotated, font=font, fill=(0, 0, 0, opacity))
angle = math.degrees(math.atan(pdf_height/pdf_width))
watermark = watermark.rotate(angle, expand=1)
# merge
wx, wy = watermark.size
px = int((pdf_width - wx)/2)
py = int((pdf_height - wy)/2)
image.paste(watermark, (px, py, px + wx, py + wy), watermark)
image.show()
Here it is in a colab https://colab.research.google.com/drive/1ERl7PiX6xKy5H9EEMulBKPgglF6euCNA?usp=sharing you should provide an example image to the colab.
I'm not saying this is going to be easy, or that this solution will necessarily be perfect for you, but look at the documentation here:
http://effbot.org/imagingbook/pil-index.htm
and especially pay attention to the Image, ImageDraw, and ImageFont modules.
Here's an example to help you out:
import Image
im = Image.new("RGB", (100, 100))
import ImageDraw
draw = ImageDraw.Draw(im)
draw.text((50, 50), "hey")
im.rotate(45).show()
To do what you really want you may need to make a bunch of separate correctly rotated text images and then compose them all together with some more fancy manipulation. And after all that it still may not look great. I'm not sure how antialiasing and such is handled for instance, but it might not be good. Good luck, and if anyone has an easier way, I'd be interested to know as well.
If you a using aggdraw, you can use settransform() to rotate the text. It's a bit undocumented, since effbot.org is offline.
# Matrix operations
def translate(x, y):
return np.array([[1, 0, x], [0, 1, y], [0, 0, 1]])
def rotate(angle):
c, s = np.cos(angle), np.sin(angle)
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
def draw_text(image, text, font, x, y, angle):
"""Draw text at x,y and rotated angle radians on the given PIL image"""
m = np.matmul(translate(x, y), rotate(angle))
transform = [m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2]]
draw = aggdraw.Draw(image)
draw.settransform(transform)
draw.text((tx, ty), text, font)
draw.settransform()
draw.flush()

Categories