Related
I have the following Python code:
FONT = cv2.FONT_HERSHEY_SIMPLEX
FONT_SCALE = 1.0
FONT_THICKNESS = 2
bg_color = (255, 255, 255)
label_color = (0, 0, 0)
label = 'Propaganda'
label_width, label_height = cv2.getTextSize(label, FONT, FONT_SCALE, FONT_THICKNESS)[0]
label_patch = np.zeros((label_height, label_width, 3), np.uint8)
label_patch[:,:] = bg_color
I create a new blank image, with the size returned by getTextSize, and then, I add the text at the bottom-left point, according to the docs, which is x = 0, y = (height - 1) and with the same font, scale and thickness parameters used for getTextSize
cv2.putText(label_patch, label, (0, label_height - 1), FONT, FONT_SCALE, label_color, FONT_THICKNESS)
But when I use imshow or imwrite on the image label_patch, this is the results I get:
It can easily be seen that lowercase p and lowercase g are cut in the middle, such that g and a cannot even be distinguished. How can I make OpenCV's getTextSize return the correct size, and how can I make OpenCV's putText start drawing the text from the actual lowest point?
Found the solution to my problem, so sharing it here.
It turns out that there is another parameter that getTextSize returns, which is the baseline. It should have been taken into account when creating the box: the box height should be label_height + baseline:
(label_width, label_height), baseline = cv2.getTextSize(label, FONT, FONT_SCALE, FONT_THICKNESS)
label_patch = np.zeros((label_height + baseline, label_width, 3), np.uint8)
label_patch[:,:] = bg_color
Now, adding the text at the same point as before, which means that the baseline pixels will remain below (the point has actually moved one pixel down, as advised by #api55):
cv2.putText(label_patch, label, (0, label_height), FONT, FONT_SCALE, label_color, FONT_THICKNESS)
And the result:
I am looking to display some images in OpenCV Python with titles and borders around the each subplot. something like this (courtesy of the following stackoverflow post: OpenCV (Python) video subplots):
WHAT I WANT:
But I only manage to get this with that code adapted.
import cv2
im1 = cv2.imread('Lenna.png')
final_frame = cv2.hconcat((im1, im1))
cv2.imshow('lena', final_frame)
WHAT I HAVE
Is it possible to obtain this using OpenCV?
I know a workaround would be to put text on the images, but that's not what I want because it will cover important information that way.
UPDATE
My bad, I didn't specify initially: I have 4 subplots (so 4 different images) and not two like in the example. Also, I want the solution to be as fast as possible since I have video (time restrictions)
I have a pretty quick and dirty solution. You can refine it to suit your needs. I have the explanation alongside the code as well:
import cv2
import numpy as np
img1 = cv2.imread('lena.jpg')
#--- Here I am creating the border---
black = [0,0,0] #---Color of the border---
constant=cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_CONSTANT,value=black )
cv2.imshow('constant',constant)
You can find many other options for different borders ON THIS PAGE
#--- Here I created a violet background to include the text ---
violet= np.zeros((100, constant.shape[1], 3), np.uint8)
violet[:] = (255, 0, 180)
#--- I then concatenated it vertically to the image with the border ---
vcat = cv2.vconcat((violet, constant))
cv2.imshow('vcat', vcat)
#--- Now I included some text ---
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(vcat,'FRAME',(30,50), font, 2,(0,0,0), 3, 0)
cv2.imshow('Text', vcat)
#--- I finally concatenated both the above images horizontally---
final_img = cv2.hconcat((vcat, vcat))
cv2.imshow('Final', final_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
The general idea would be to create a new image with width += width/10 and height += height/20. Write some text as heading and place the input image along the center as:
import cv2
import numpy as np
img = cv2.imread("/Users/anmoluppal/Downloads/Lenna.png")
height, width, ch = img.shape
new_width, new_height = width + width/20, height + height/8
# Crate a new canvas with new width and height.
canvas = np.ones((new_height, new_width, ch), dtype=np.uint8) * 125
# New replace the center of canvas with original image
padding_top, padding_left = 60, 10
if padding_top + height < new_height and padding_left + width < new_width:
canvas[padding_top:padding_top + height, padding_left:padding_left + width] = img
else:
print "The Given padding exceeds the limits."
text1 = "Sample Image 1"
text2 = "Sample Image 2"
img1 = cv2.putText(canvas.copy(), text1, (int(0.25*width), 30), cv2.FONT_HERSHEY_COMPLEX, 1, np.array([255, 0, 0]))
img2 = cv2.putText(canvas.copy(), text2, (int(0.25*width), 30), cv2.FONT_HERSHEY_COMPLEX, 1, np.array([255, 0, 0]))
final = cv2.hconcat((img1, img2))
cv2.imwrite("./debug.png", final)
I used the other answers to make a generalizable function which works for arbitrary row/columns:
def cvSubplot(imgs, # 2d np array of imgs (each img an np arrays of depth 1 or 3).
pad=10, # number of pixels to use for padding between images. must be even
titles=None, # (optional) np array of subplot titles
win_name='CV Subplot' # name of cv2 window
):
'''
Makes cv2 based subplots. Useful to plot image in actual pixel size
'''
rows, cols = imgs.shape
subplot_shapes = np.array([list(map(np.shape, x)) for x in imgs])
sp_height, sp_width, depth = np.max(np.max(subplot_shapes, axis=0), axis=0)
title_pad = 30
if titles is not None:
pad_top = pad + title_pad
else:
pad_top = pad
frame = np.zeros((rows*(sp_height+pad_top), cols*(sp_width+pad), depth ))
for r in range(rows):
for c in range(cols):
img = imgs[r, c]
h, w, _ = img.shape
y0 = r * (sp_height+pad_top) + pad_top//2
x0 = c * (sp_width+pad) + pad//2
frame[y0:y0+h, x0:x0+w, :] = img
if titles is not None:
frame = cv2.putText(frame, titles[r, c], (x0, y0-title_pad//4), cv2.FONT_HERSHEY_COMPLEX, .5, (255,255,255))
cv2.imshow(win_name, frame)
cv2.waitKey(0)
Below is an example usage:
import cv2
import numpy as np
a1 = np.random.random((40,400,1))
a2 = np.random.random((200,200,1))
a3 = np.random.random((100,100,1))
a4 = np.random.random((300,150,1))
a5 = np.random.random((100,150,1))
filler = np.zeros((0,0,1))
titles = np.array([['A', 'B', 'C'], ['D', 'E', 'Filler']])
imgs = np.array([[a1, a2, a3], [a4, a5, filler]])
cvSubplot(imgs, pad=20, titles=titles)
That script produces the following cv2 image:
I want to make thumb and crop it to needed size. It works fine, but if my new thumb area is smaller than crop one, all empty space fills with black color.
Code:
import os
from PIL import Image
def resize(file_path):
file, ext = os.path.splitext(file_path)
im = Image.open(file_path)
size = (100, 'auto')
new_path = file + "_.jpg"
im.thumbnail(size, Image.ANTIALIAS)
region = im.crop((0, 0, 100, 100))
region.save(new_path, "JPEG")
Maybe there is some option like max_height for crop method or anything else?
Thanks!
You will need to apply some simple algorithm there instead of a blind cropping.
Get the square of maximum size possible in the image with square center aligning with the center of the image.
Square of maximum size would be having side equal to max of height or width of the image.
After getting the square, resample it to the size of your thumbnail dimensions.
This should work fine for most images, however if you are generating thumbnails for face images, this might not be a good method, and you might need some face recognition techniques for better output.
Are you trying to only conditionally crop the image if its LARGER than 100x100?
If so,
def resize(file_path):
file, ext = os.path.splitext(file_path)
im = Image.open(file_path)
size = (100, 'auto')
new_path = file + "_.jpg"
im.thumbnail(size, Image.ANTIALIAS)
if im.size[1] > 100:
im = im.crop((0, 0, 100, 100))
im.save(new_path, "JPEG")
I found solution:
import os
from PIL import Image
def resize(file_path):
file, ext = os.path.splitext(file_path)
im = Image.open(file_path)
size = (100, 'auto')
new_path = file + "_.jpg"
im.thumbnail(size)
(width, height) = im.size
if height >= width: box = (0, 0, 100, 100)
else: box = (0, 0, 100, height)
region = im.crop(box)
region.save(new_path, "JPEG")
Thanks for your responses!
I would do it this way:
If the image is wide, then scale it to be 100px tall. If it's tall, scale it to be 100px wide.
Crop out the middle 100x100.
def resize(file_path):
file, ext = os.path.splitext(file_path)
im = Image.open(file_path)
w, h = im.size
size = (100, 'auto') if h > w else ('auto', 100)
new_path = file + "_.jpg"
im.thumbnail(size, Image.ANTIALIAS)
w, h = im.size
region = im.crop((w/2 - 50, h/2 - 50, w/2 + 50, h/2 + 50))
region.save(new_path, "JPEG")
How would I center-align (and middle-vertical-align) text when using PIL?
Deprecation Warning: textsize is deprecated and will be removed in Pillow 10 (2023-07-01). Use textbbox or textlength instead.
Code using textbbox instead of textsize.
from PIL import Image, ImageDraw, ImageFont
def create_image(size, bgColor, message, font, fontColor):
W, H = size
image = Image.new('RGB', size, bgColor)
draw = ImageDraw.Draw(image)
_, _, w, h = draw.textbbox((0, 0), message, font=font)
draw.text(((W-w)/2, (H-h)/2), message, font=font, fill=fontColor)
return image
myFont = ImageFont.truetype('Roboto-Regular.ttf', 16)
myMessage = 'Hello World'
myImage = create_image((300, 200), 'yellow', myMessage, myFont, 'black')
myImage.save('hello_world.png', "PNG")
Result
Use Draw.textsize method to calculate text size and re-calculate position accordingly.
Here is an example:
from PIL import Image, ImageDraw
W, H = (300,200)
msg = "hello"
im = Image.new("RGBA",(W,H),"yellow")
draw = ImageDraw.Draw(im)
w, h = draw.textsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, fill="black")
im.save("hello.png", "PNG")
and the result:
If your fontsize is different, include the font like this:
myFont = ImageFont.truetype("my-font.ttf", 16)
draw.textsize(msg, font=myFont)
Here is some example code which uses textwrap to split a long line into pieces, and then uses the textsize method to compute the positions.
from PIL import Image, ImageDraw, ImageFont
import textwrap
astr = '''The rain in Spain falls mainly on the plains.'''
para = textwrap.wrap(astr, width=15)
MAX_W, MAX_H = 200, 200
im = Image.new('RGB', (MAX_W, MAX_H), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(
'/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', 18)
current_h, pad = 50, 10
for line in para:
w, h = draw.textsize(line, font=font)
draw.text(((MAX_W - w) / 2, current_h), line, font=font)
current_h += h + pad
im.save('test.png')
One shall note that the Draw.textsize method is inaccurate. I was working with low pixels images, and after some testing, it turned out that textsize considers every character to be 6 pixel wide, whereas an I takes max. 2 pixels and a W takes min. 8 pixels (in my case). And so, depending on my text, it was or wasn't centered at all. Though, I guess "6" was an average, so if you're working with long texts and big images, it should still be ok.
But now, if you want some real accuracy, you better use the getsize method of the font object you're going to use:
arial = ImageFont.truetype("arial.ttf", 9)
w,h = arial.getsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, font=arial, fill="black")
As used in Edilio's link.
A simple solution if you're using PIL 8.0.0 or above: text anchors
width, height = # image width and height
draw = ImageDraw.draw(my_image)
draw.text((width/2, height/2), "my text", font=my_font, anchor="mm")
mm means to use the middle of the text as anchor, both horizontally and vertically.
See the anchors page for other kinds of anchoring. For example if you only want to center horizontally you may want to use ma.
The PIL docs for ImageDraw.text are a good place to start, but don't answer your question.
Below is an example of how to center the text in an arbitrary bounding box, as opposed to the center of an image. The bounding box is defined as: (x1, y1) = upper left corner and (x2, y2) = lower right corner.
from PIL import Image, ImageDraw, ImageFont
# Create blank rectangle to write on
image = Image.new('RGB', (300, 300), (63, 63, 63, 0))
draw = ImageDraw.Draw(image)
message = 'Stuck in\nthe middle\nwith you'
bounding_box = [20, 30, 110, 160]
x1, y1, x2, y2 = bounding_box # For easy reading
font = ImageFont.truetype('Consolas.ttf', size=12)
# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font)
# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w)/2 + x1
y = (y2 - y1 - h)/2 + y1
# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font)
# Draw the bounding box to show that this works
draw.rectangle([x1, y1, x2, y2])
image.show()
image.save('text_center_multiline.png')
The output shows the text centered vertically and horizontally in the bounding box.
Whether you have a single or multiline message no longer matters, as PIL incorporated the align='center' parameter. However, it is for multiline text only. If the message is a single line, it needs to be manually centered. If the message is multiline, align='center' does the work for you on subsequent lines, but you still have to manually center the text block. Both of these cases are solved at once in the code above.
Use the textsize method (see docs) to figure out the dimensions of your text object before actually drawing it. Then draw it starting at the appropriate coordinates.
All the other answers did NOT take text ascender into consideration.
Here's a backport of ImageDraw.text(..., anchor="mm"). Not sure if it's fully compatible with anchor="mm", cause I haven't tested the other kwargs like spacing, stroke_width yet. But I ensure you this offset fix works for me.
from PIL import ImageDraw
from PIL import __version__ as pil_ver
PILLOW_VERSION = tuple([int(_) for _ in pil_ver.split(".")[:3]])
def draw_anchor_mm_text(
im,
xy,
# args shared by ImageDraw.textsize() and .text()
text,
font=None,
spacing=4,
direction=None,
features=None,
language=None,
stroke_width=0,
# ImageDraw.text() exclusive args
**kwargs,
):
"""
Draw center middle-aligned text. Basically a backport of
ImageDraw.text(..., anchor="mm").
:param PIL.Image.Image im:
:param tuple xy: center of text
:param unicode text:
...
"""
draw = ImageDraw.Draw(im)
# Text anchor is firstly implemented in Pillow 8.0.0.
if PILLOW_VERSION >= (8, 0, 0):
kwargs.update(anchor="mm")
else:
kwargs.pop("anchor", None) # let it defaults to "la"
if font is None:
font = draw.getfont()
# anchor="mm" middle-middle coord xy -> "left-ascender" coord x'y'
# offset_y = ascender - top, https://stackoverflow.com/a/46220683/5101148
# WARN: ImageDraw.textsize() return text size with offset considered.
w, h = draw.textsize(
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
offset = font.getoffset(text)
w, h = w - offset[0], h - offset[1]
xy = (xy[0] - w / 2 - offset[0], xy[1] - h / 2 - offset[1])
draw.text(
xy,
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
**kwargs,
)
Refs
https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html
https://github.com/python-pillow/Pillow/issues/4789
https://stackoverflow.com/a/46220683/5101148
https://github.com/python-pillow/Pillow/issues/2486
Using a combination of anchor="mm" and align="center" works wonders. Example
draw.text(
xy=(width / 2, height / 2),
text="centered",
fill="#000000",
font=font,
anchor="mm",
align="center"
)
Note: Tested where font is an ImageFont class object constructed as such:
ImageFont.truetype('path/to/font.ttf', 32)
This is a simple example to add a text in the center of the image
from PIL import Image, ImageDraw, ImageFilter
msg = "hello"
img = Image.open('image.jpg')
W, H = img.size
box_image = img.filter(ImageFilter.BoxBlur(4))
draw = ImageDraw.Draw(box_image)
w, h = draw.textsize(msg)
draw.text(((W - w) / 2, (H - h) / 2), msg, fill="black")
box_image.show()
if you are using the default font then you can use this simple calculation
draw.text((newimage.width/2-len(text)*3, 5), text,fill="black", align ="center",anchor="mm")
the main thing is
you have to divide the image width by 2 then get the length of the string you want and multiply it by 3 and subtract it from the division result
newimage.width/2-len(text)*3 #this is X position
**this answer is an estimation for the default font size used if you use a custom font then the multiplier must be changed accordingly. in the default case it is 3
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()