Saving GIF with PIL in Python becomes flickery black/white - python

I am trying to create a small section above a GIF that a user inputs along with some text they can add to mimic that popular GIF meme style. Upon output, the colors of the gif are entirely completely black or completely white with lots of artifacting.
I open the input GIF and create a new image using PIL with the same dimensions, plus a little extra height.
giftemplate = Image.open("input.gif")
# create empty white frame with extra height for text
result_template = Image.new(giftemplate.mode, size=(base_width, new_height), color=(255, 255, 255))
Then I add some text to the extra height and loop over every frame in the gif, pasting the current frame to the new image, then append that frame in a list.
# paste each frame of gif under extra height
frames = []
for frame in ImageSequence.Iterator(giftemplate):
result_template.paste(frame, (0, padding_size))
b = BytesIO()
result_template.save(b, format="GIF")
result_frame = Image.open(b)
frames.append(result_frame)
frames[0].save('meme_out.gif', save_all=True, append_images=frames[1:], loop=0)
Gif used as input -
Input.gif
The generated gif outputted by PIL - meme_out.gif
Notice the total lack of color, and (while not so apparent on this example) slowed speed. You can still see a small outline of the cat dancing.
The output gif looks totally fine if I dont paste each frame of the gif onto created image with text
EDIT: Reproducible example provided:
from PIL import Image, ImageFont, ImageDraw, ImageSequence
from io import BytesIO
caption = "sample"
giftemplate = Image.open("input.gif")
font = ImageFont.truetype("Futura Condensed Extra Bold Regular.ttf", 10)
# text margin size scales with image height
text_margin = int((giftemplate.height / 100))
# text width and height
tw, th = font.getsize(caption)
# top left text box coordinate with respect to image pixels. Top left of image is 0,0
cx, cy = int(giftemplate.width / 2), text_margin
padding_size = th
base_width, base_height = giftemplate.size
new_height = base_height + padding_size
# create empty white frame with extra height for text
result_template = Image.new(giftemplate.mode, size=(base_width, new_height), color=(255, 255, 255))
# draw text lines in the extra height
tw, th = font.getsize(caption)
draw = ImageDraw.Draw(result_template)
draw.text((cx - tw / 2, cy), caption, (0, 0, 0), font=font)
# paste each frame of gif under extra height
frames = []
for frame in ImageSequence.Iterator(giftemplate):
result_template.paste(frame, (0, padding_size))
b = BytesIO()
result_template.save(b, format="GIF")
result_frame = Image.open(b)
frames.append(result_frame)
frames[0].save('meme_out.gif', save_all=True, append_images=frames[1:], loop=0)

Related

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:

How to merge multiple pictures diagonally into a single one using Python

I'm trying to merge multiple images diagonally into a single one using Python.
I checked a lot of questions but didn't find something similar to my need.
All I can do right now is a simple merge of files on top of each other:
from PIL import Image
import numpy as np
img = Image.open("1.png")
background = Image.open("2.png")
background.paste(img, (0, 0), img)
background.save('result.png',"PNG")
Here are the pictures to test :
image1, image2, image3
I need the pictures to be arranged diagonally to fit into a final 900 x 1200 px size picture with white Background. Probably they need to be sized down a bit and fit ? At least that's the process I am doing in Photoshop, manually (time consuming).
Sometimes there's 2 pictures to fit, sometimes could be 4 or 5.
This should do the job:
from PIL import Image
images = ['1.png', '2.png', '3.png']
# shift between images
offset = (200, 100)
target_size = (900, 1200)
images = [Image.open(fn) for fn in images]
no_img = len(images)
image_size = [s+no_img*o for s, o in zip(images[0].size, offset)]
#create empty background
combined_image = Image.new('RGBA', image_size)
# paste each image at a slightly shifted position, start at top right
for idx, image in enumerate(images):
combined_image.paste(image, ((no_img - idx - 1) * offset[0], idx * offset[1]), image)
# crop to non-empty area
combined_image = combined_image.crop(combined_image.getbbox())
# resizing and padding such that it fits 900 x 1200 px
scale = min(target_size[0] / combined_image.size[0], target_size[1] / combined_image.size[1])
combined_image = combined_image.resize((int(combined_image.size[0] * scale), int(combined_image.size[1] * scale)), Image.BICUBIC)
img_w, img_h = combined_image.size
finale_output = Image.new('RGB', target_size, (255, 255, 255))
offset = ((target_size[0] - img_w) // 2, (target_size[1] - img_h) // 2)
finale_output.paste(combined_image, offset, combined_image)
# display
finale_output.show()
EDIT: I added the code for resizing and padding such that the output is exactly of your wanted size (whilst maintaining the aspect ratio).

How can I randomly add 20 images(10x10) in an empty background image (200x200) in python?

I want the 10 small images to be placed in this circle
I'm working on a small project to randomly place or put several images of size (10 w x 10 h) in another image that will be used as background of size (200 w x 200 h) in python. The small images should be put at a random location in the background image.
I have 20 small images of size (10x10) and one empty image background of size (200x200). I want to put my 20 small images in the empty background image at a random location in the background.
Is there a way to do it in Python?
Code
# Depencies importation
import cv2
# Saving directory
saving_dir = "../Saved_Images/"
# Read the background image
bgimg = cv2.imread("../Images/background.jpg")
# Resizing the bacground image
bgimg_resized = cv2.resize(bgimg, (2050,2050))
# Read the image that will be put in the background image (exemple of 1)
small_img = cv2.imread("../Images/small.jpg")
# Convert the resized background image to gray
bgimg_gray = cv2.cvtColor(bgimg, cv2.COLOR_BGR2GRAY)
# Convert the grayscale image to a binary image
ret, thresh = cv2.threshold(bgimg_gray,127,255,0)
# Determine the moments of the binary image
M = cv2.moments(thresh)
# calculate x,y coordinate of center
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# drawing the circle in the background image
circle = cv2.circle(bgimg, (cX, cY), 930, (0,0,255), 9)
print(circle)
# Saving the new image
cv2.imwrite(saving_dir+"bgimg"+".jpg", bgimg)
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.resizeWindow("Test", 1000, 1200)
# Showing the images
cv2.imshow("image", bgimg)
# Waiting for any key to stop the program execution
cv2.waitKey(0)
the above code is for one image, I want to do it for the 20 and to put them in a random location
Assuming you have that background image background.jpg (decreased to 200x200 px) and 10 images: image01.png, image02.png ... image10.png (10x10 px). Then:
import glob
import random
from PIL import Image
img_bg = Image.open('circle.jpg')
width, height = img_bg.size
images = glob.glob('*.png')
for img in images:
img = Image.open(img)
x = random.randint(40, width-40)
y = random.randint(40, height-40)
img_bg.paste(img, (x, y, x+10, y+10))
img_bg.save('result.png', 'PNG')
Output image:

Transparency not working consistently while saving GIF's in PIL

I am working on script that writes over images and makes the background transparent. Output is supposed to be in GIF format.
The script works but for certain images the transparency is not working as expected.
Here is the script
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
CANVAS_HEIGHT = 354
CANVAS_WIDTH = 344
def get_text_mask():
font_style_path = 'Ultra-Regular.ttf'
text_mask_base = Image.new('L', (CANVAS_WIDTH, CANVAS_HEIGHT), 255)
text_mask = text_mask_base.copy()
text_mask_draw = ImageDraw.Draw(text_mask)
font = ImageFont.truetype(font_style_path, 94)
text_mask_width, text_mask_height = text_mask_draw.multiline_textsize("1000\nUsers",
font=font)
text_mask_draw.multiline_text(((CANVAS_WIDTH - text_mask_width) / 2,
(CANVAS_HEIGHT - text_mask_height) / 2),
"1000\nUsers",
font=font,
fill=0,
align='center')
return text_mask
def run():
images = ['image1.png', 'image2.png']
for index, original_image in enumerate(images):
image = Image.open(original_image)
blank_canvas = Image.new('RGBA', (CANVAS_WIDTH, CANVAS_HEIGHT), (255, 255, 255, 0))
text_mask = get_text_mask()
final_canvas = blank_canvas.copy()
for i in xrange(0, CANVAS_WIDTH, image.width):
for j in xrange(0, CANVAS_HEIGHT, image.height):
final_canvas.paste(image, (i, j))
final_canvas.paste(text_mask, mask=text_mask)
final_canvas.convert('P', palette=Image.ADAPTIVE)
final_canvas.save("output-{}.gif".format(index), format="GIF", transparency=0)
run()
image1.png
image2.png
And the font is here
https://bipuljain.com/static/images/Ultra-Regular.ttf
And the output with issue.
And output working fine.
The problem is that your "original image" contains the same index-color that the GIFs use to signify "this pixel is transparent".
Gifs are "palette-based" - one index into this palette is designated as "this is transparent" (see f.e. https://en.wikipedia.org/wiki/GIF)
So if you specify pure black or pure white as color-index that is transparent and your sourceimage already contains pixels with this exact color, they get to be transperent as well.
To avoid this, you could sample your source background-image and choose a "non-existent" color as transparency-color - this would never get to be in your resulting image.
You could also change your source images pixel values - check all pixel and change all "background-ones" a tiny fraction off so they do not get translucent.

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