Using Pillow to batch create animated GIFs - python

I am trying to use Python to batch edit .png files using the Pillow library. This is my first script using python and as such it is likely that there will be many errors and/or bad programming practise.
Here is my current code:
from PIL import Image
from PIL import ImageDraw
from os.path import basename
import os, sys
path = "D:\Pokemon Game\Pokemon Eggs\Import"
dirs = os.listdir( path )
box = (2,1,30,31)
moveup = (0, -3, -7, -11, -15, -19, -15, -9, -5, 2, 12, 14, 16, 17, 12, 8, 4, 0, -7, -13, -19, -11, -7, -5, -3)
topspace = (36, 38, 42, 46, 50, 55, 50, 44, 40, 34, 24, 22, 20, 18, 24, 28, 32, 36, 42, 48, 55, 46, 42, 40, 38)
bottomspace = (0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 6, 10, 14, 17, 12, 8, 4, 0, 0, 0, 0, 0, 0, 0, 0)
Imagesizes = ((56, 60), (56, 58), (56, 54), (56, 50), (56, 46), (56, 41), (56, 46), (56, 52), (56, 56), (56, 60), (56, 66), (56, 64), (56, 62), (56, 60), (56, 60), (56, 60), (56, 60), (56, 60), (56, 54), (56, 48), (56, 41), (56, 50), (56, 54), (56, 56), (56, 58))
for file in dirs:
#Pick an image file you have in the working directory
egg = Image.open("D:\Pokemon Game\Pokemon Eggs\Import\%s" % str(file))
#Crops white out of image
egg = egg.crop(box)
#Resizes image
egg = egg.resize((56, 60), Image.NEAREST)
#Stretch individual images
frames = []
for size in Imagesizes:
frames.append(egg.resize(size, Image.NEAREST))
#Resize canvas for each individual image - make sure it is clear
for i,image in enumerate(frames):
canvassize = (-20, -36 + moveup[i], 76, 60 + moveup[i])
frames[i]=image.crop(canvassize)
#Fix transparency
for i,image in enumerate(frames):
transparent_area1 = (0,0,20,96)
transparent_area2 = (76,0,96,96)
transparent_area3 = (0,0,96,(topspace[i]))
transparent_area4 = (0,(96-bottomspace[i]), 96, 96)
image.convert('RGBA')
mask=Image.new("1", (96, 96), color=255)
draw=ImageDraw.Draw(mask)
draw.rectangle(transparent_area1, fill=0)
draw.rectangle(transparent_area2, fill=0)
draw.rectangle(transparent_area3, fill=0)
draw.rectangle(transparent_area4, fill=0)
image.putalpha(mask)
#Convert to GIF
My aim is to make the inanimate egg image end up like the animated image shown below:
The issues I have with my code are that firstly, the whole section between line 35 and line 47 results in the loss of transparancy (this is due to line 47). And I do not know how I would convert the list (image) into an animated GIF.

Instead of using 'resize' as a reverse crop, I would suggest creating a new image, and pasting the original image on top of it. This solves the issue of the black new pixels.
When pasting, you can supply a mask - https://pillow.readthedocs.io/en/5.2.x/reference/Image.html#PIL.Image.Image.paste - which will mean that the original image is pasted only in the parts indicated by mask, rather than a simple rectangle.
For a few of the items in the next part, https://pillow.readthedocs.io/en/5.2.x/handbook/image-file-formats.html#saving explains further.
When saving, GIF uses the additional parameters 'transparency' and 'disposal'. Disposal is used to control how the next frame uses the previous one - is the new frame pasted on top of it? Does it ignore it?
As for creating an animated GIF, Pillow has the arguments 'save_all', to save a multiframe image, and 'append_images' to add the additional frames to the first image.
So, change the lower part of your code to this -
#Resize canvas for each individual image - make sure it is clear
for i,image in enumerate(frames):
frame = Image.new('RGBA', (76 + 20, 60 + moveup[i] - (-36 + moveup[i])), '#fff')
image = image.convert('RGBA')
frame.paste(image, (20, 36 - moveup[i]), image)
frames[i] = frame
#Convert to GIF
frames[0].save('out.gif', save_all=True, append_images=frames[1:], transparency=0, disposal=2)

Related

Change Palette color index in Python

I got this image.
The image is PNG, in mode P, palette is mode RGB.
I need to stay with 16 colors, as I want the image as 4bpp.
And I need to change his palette, making the color pink (255, 192, 203) its first index.
The image palette is:
{(255, 255, 232): 0, (255, 192, 203): 1, (210, 204, 147): 2, (62, 214, 108): 3, (59, 193, 95): 4, (209, 174, 99): 5, (194, 164, 92): 6, (130, 186, 185): 7, (180, 148, 83): 8, (95, 152, 121): 9, (49, 161, 88): 10, (157, 123, 59): 11, (118, 97, 55): 12, (52, 128, 119): 13, (73, 80, 63): 14, (63, 59, 47): 15}
And I want:
{(255, 192, 203): 0, (255, 255, 232): 1, (210, 204, 147): 2, (62, 214, 108): 3, (59, 193, 95): 4, (209, 174, 99): 5, (194, 164, 92): 6, (130, 186, 185): 7, (180, 148, 83): 8, (95, 152, 121): 9, (49, 161, 88): 10, (157, 123, 59): 11, (118, 97, 55): 12, (52, 128, 119): 13, (73, 80, 63): 14, (63, 59, 47): 15}
I made this code for it:
def change_palette(im):
colors = im.palette.colors #get colors from palette
if(PINK not in colors): #check there is pink (could be possible that there is not)
return
first = list(colors.keys())[list(colors.values()).index(0)] #colors is a dict so I get the key of the first index
colors[first] = colors[PINK]
colors[PINK] = 0 #change the value of the colors
newcolors = {}
newcolors[PINK] = colors.pop(PINK)
for key in colors:
newcolors[key] = colors[key] #reorder the dict so it is in order
colors = newcolors
newcolors = []
for key in colors:
for c in key:
newcolors.append(c) #make it a list
im.putpalette(newcolors) #change palette
But it makes the image look like this.
I tried it with other methods, like not using the putpalette and changing only the colors of the palette, but that does not make any change when I save the image.
I understand the problem, but I can't seem to find a solution.
I want it to look exactly the same, but with the color pink (255, 192, 203) in the first index.
If I understand correctly, you want to keep the image unchanged, and replace the index of the pink color to be 0.
When we modify the palette, we are switching between the two colors:
All the pixels with color (255, 255, 232) are switched to pink color (255, 192, 203), and all the pixels with pink color (255, 192, 203) are switched to color (255, 255, 232).
The reason is that the (index) values of the pixels are not changed.
After applying the new palette, all the pixels with (index) value 0 turned to be pink, and all the (index) value 1 turned to be (255, 255, 232).
For fixing that, we have to switch the image data as well (assuming pink color index equals 1):
All the pixels with (index) value 0 should be modified to be value 1.
All the pixels with (index) value 1 should be modified to be value 0.
For convenience we may convert the data to NumPy array, and convert back to PIL Image:
pink_index = colors[PINK] # Original index of pink color
...
indexed = np.array(im) # Convert to NumPy array to easier access.
new_indexed = indexed.copy() # Make a copy of the NumPy array
new_indexed[indexed == 0] = pink_index # Replace all original 0 pixels with pink_index
new_indexed[indexed == pink_index] = 0 # Replace all original pink_index pixels with 0
new_im = Image.fromarray(new_indexed) # Convert from NumPy array to Image.
new_im.putpalette(newcolors) # Set the palette
Complete code sample:
from PIL import Image
import numpy as np
PINK = (255, 192, 203)
def change_palette(im):
colors = im.palette.colors #get colors from palette
if(PINK not in colors): #check there is pink (could be possible that there is not)
return
first = list(colors.keys())[list(colors.values()).index(0)] #colors is a dict so I get the key of the first index
pink_index = colors[PINK]
colors[first] = colors[PINK]
colors[PINK] = 0 #change the value of the colors
newcolors = {}
newcolors[PINK] = colors.pop(PINK)
for key in colors:
newcolors[key] = colors[key] #reorder the dict so it is in order
colors = newcolors
newcolors = []
for key in colors:
for c in key:
newcolors.append(c) #make it a list
#im.putpalette(newcolors) #change palette
indexed = np.array(im) # Convert to NumPy array to easier access https://stackoverflow.com/a/33023875/4926757
new_indexed = indexed.copy() # Make a copy of the NumPy array
new_indexed[indexed == 0] = pink_index # Replace all original 0 pixels with pink_index
new_indexed[indexed == pink_index] = 0 # Replace all original pink_index pixels with 0
new_im = Image.fromarray(new_indexed) # Convert from NumPy array to Image https://stackoverflow.com/a/39258561/4926757
new_im.putpalette(newcolors) # Set the palette
return new_im
img = Image.open('original_image.png')
new_image = change_palette(img)
new_image.save('changed_image.png')
Result:

Can I search for black pixels in a photo and find out where they are in the photo using python?

I want to search for black pixels in a screenshot I took using pyautogui and I want to find the x and y location of those pixels using python so that I can move the mouse to the black pixels locations using pynput. I tried using imageio but I could not find a command that would do what I want. I asked this a few hours Ago but it was closed so I made the necessary edits to it.
Here is one way in Python/OpenCV/Numpy using np.argwhere on a thresholded image that isolates the black spots.
Read the input
Threshold using inRange on black and invert
Use np.argwhere to located the coordinates of the black pixels in the mask
Print the results
Input (4 black clusters near the 4 corners):
import cv2
import numpy as np
# read input
img = cv2.imread("lena_black_spots.png")
low = (0,0,0)
high = (0,0,0)
mask = cv2.inRange(img, low, high)
mask = 255 - mask
# find black coordinates
coords = np.argwhere(mask==0)
for p in coords:
pt = (p[0],p[1])
print (pt)
# save output
cv2.imwrite('lena_black_spots_mask.png', mask)
cv2.imshow('img', img)
cv2.imshow('mask', mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
Mask:
Coordinates:
(18, 218)
(18, 219)
(19, 218)
(19, 219)
(20, 218)
(20, 219)
(38, 21)
(38, 22)
(39, 21)
(39, 22)
(173, 244)
(173, 245)
(174, 244)
(174, 245)
(194, 23)
(194, 24)
(195, 23)
(195, 24)
I'd recommend looking into the ImageMagick library. That's the go-to library for working with images in Python.

How to draw lines between points in OpenCV?

I have an array of tuples:
a = [(375, 193)
(364, 113)
(277, 20)
(271, 16)
(52, 106)
(133, 266)
(289, 296)
(372, 282)]
How to draw lines between points in OpenCV?
Here is my code that isn't working:
for index, item in enumerate(a):
print (item[index])
#cv2.line(image, item[index], item[index + 1], [0, 255, 0], 2)
Using draw contours, you can draw the shape all at once.
img = np.zeros([512, 512, 3],np.uint8)
a = np.array([(375, 193), (364, 113), (277, 20), (271, 16), (52, 106), (133, 266), (289, 296), (372, 282)])
cv2.drawContours(img, [a], 0, (255,255,255), 2)
If you don't want the image closed and want to continue how you started:
image = np.zeros([512, 512, 3],np.uint8)
pointsInside = [(375, 193), (364, 113), (277, 20), (271, 16), (52, 106), (133, 266), (289, 296), (372, 282)]
for index, item in enumerate(pointsInside):
if index == len(pointsInside) -1:
break
cv2.line(image, item, pointsInside[index + 1], [0, 255, 0], 2)
Regarding your current code, it looks like you are trying to access the next point by indexing the current point. You need to check for the next point in the original array.
A more Pythonic way of doing the second version would be:
for point1, point2 in zip(a, a[1:]):
cv2.line(image, point1, point2, [0, 255, 0], 2)
If you just want to draw lines, how about cv2.polylines? cv2.drawContours would be preferred when you already have a contours object.
cv2.polylines(image,
a,
isClosed = False,
color = (0,255,0),
thickness = 3,
linetype = cv2.LINE_AA)

How to create image from a list of pixel values in Python3?

If I have a list of pixel rows from an image in the following format, how would get the image?
[
[(54, 54, 54), (232, 23, 93), (71, 71, 71), (168, 167, 167)],
[(204, 82, 122), (54, 54, 54), (168, 167, 167), (232, 23, 93)],
[(71, 71, 71), (168, 167, 167), (54, 54, 54), (204, 82, 122)],
[(168, 167, 167), (204, 82, 122), (232, 23, 93), (54, 54, 54)]
]
PIL and numpy are your friends here:
from PIL import Image
import numpy as np
pixels = [
[(54, 54, 54), (232, 23, 93), (71, 71, 71), (168, 167, 167)],
[(204, 82, 122), (54, 54, 54), (168, 167, 167), (232, 23, 93)],
[(71, 71, 71), (168, 167, 167), (54, 54, 54), (204, 82, 122)],
[(168, 167, 167), (204, 82, 122), (232, 23, 93), (54, 54, 54)]
]
# Convert the pixels into an array using numpy
array = np.array(pixels, dtype=np.uint8)
# Use PIL to create an image from the new array of pixels
new_image = Image.fromarray(array)
new_image.save('new.png')
EDIT:
A little fun with numpy to make an image of random pixels:
from PIL import Image
import numpy as np
def random_img(output, width, height):
array = np.random.random_integers(0,255, (height,width,3))
array = np.array(array, dtype=np.uint8)
img = Image.fromarray(array)
img.save(output)
random_img('random.png', 100, 50)
Haven't used PIL myself, but the best way of finding out is to open an actual image file using PIL. Then explore the API and objects involved with opening said image and look at how the pixel values are stored within the specific object pertaining to the API.
You can then construct a valid PIL image object using your extracted RGB values.
Edit:
See the following post: How do I create an image in PIL using a list of RGB tuples?
Additional, accessing pixel values in PIL: https://pillow.readthedocs.io/en/4.3.x/reference/PixelAccess.html

Python Pil Change GreyScale Tif To RGB

I have a greyscale TIF File. I need to convert it to RGB/ read from it in a way I can work with it.
img = Image.open(GIF_FILENAME)
rgbimg = img.convert('RGB')
for i in range(5):
print rgbimg.getpixel((i, 0))
The convert.("RGB") will automatically make everything (255,255,255) even though the picture is a really dark mostly black picture.
If I just read the greyscale numbers I get numbers from around 1400 to 1900.
I need to also save a copy of the picture as a RGB Jpeg.
Picture in question: [Here]: http://imgur.com/kEwfFs3
How would I go along doing this?
what about:
img = Image.open(GIF_FILENAME)
rgbimg = Image.new("RGBA", img.size)
rgbimg.paste(img)
rgbimg.save('foo.jpg')
[EDIT]
created a test:
from PIL import Image
from collections import defaultdict
import pprint
img = Image.open("kEwfFs3.png")
rgbimg = Image.new("RGBA", img.size)
rgbimg.paste(img)
found_colors = defaultdict(int)
for x in range(0, rgbimg.size[0]):
for y in range(0, rgbimg.size[1]):
pix_val = rgbimg.getpixel((x, y))
found_colors[pix_val] += 1
pprint.pprint(dict(found_colors))
rgbimg.save('kEwfFs3.jpg')
And that outputs:
{(0, 0, 0, 255): 747802,
(1, 1, 1, 255): 397,
(2, 2, 2, 255): 299,
(3, 3, 3, 255): 255,
(4, 4, 4, 255): 221,
(5, 5, 5, 255): 200,
(6, 6, 6, 255): 187,
(7, 7, 7, 255): 138,
(8, 8, 8, 255): 160,
(9, 9, 9, 255): 152,
(10, 10, 10, 255): 122,
(11, 11, 11, 255): 116,
(12, 12, 12, 255): 144,
(13, 13, 13, 255): 117,
(14, 14, 14, 255): 117,
(15, 15, 15, 255): 102,
(16, 16, 16, 255): 119,
(17, 17, 17, 255): 299641,
(18, 18, 18, 255): 273,
(19, 19, 19, 255): 233,
.................... etc .......
.................... etc .......
(249, 249, 249, 255): 616,
(250, 250, 250, 255): 656,
(251, 251, 251, 255): 862,
(252, 252, 252, 255): 1109,
(253, 253, 253, 255): 1648,
(254, 254, 254, 255): 2964175}
Which is what you would expect.
Is your output different?
I ran into the same problem with an I;16 (16-bit grayscale) tiff, converted to RGB. Some digging into the manual reveals the problem has to do with the lut PIL is using to convert grayscale images to RGB. It is working in an 8-bit color space; that is it clips all values above 255. So a quick and simple solution is to manually convert to RGB using your own lut which scales the values within range using the point method like so:
path = 'path\to\image'
img = Image.open(path)
img.point(lambda p: p*0.0039063096, mode='RGB')
img = img.convert('RGB')
img.show() # check it out!
I determined the "lut" formula by just dividing 256 by the 16-bit equivalent, 65535.

Categories

Resources