New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Are frames compressed or changed when creating gifs? #3660
Comments
I suspect as each frame is re-saved it's going through the GIF encoder afresh. |
To make it clearer: #!/usr/bin/env python3
'''
This illustrates the problem I'm having editing gifs.
'''
import numpy
from PIL import Image
# Open the gif.
def gif_open(filename):
image = Image.open(filename)
frames = []
try:
while True:
frames.append(numpy.array(image.convert('RGB')))
image.seek(image.tell()+1)
except EOFError:
pass
return numpy.asarray(frames) # Transform list of frames into numpy array for convenience.
frames = gif_open('gif.gif')
frames[0][0][0][0] = 222
frames[0][0][0][1] = 111
frames[0][0][0][2] = 123
print('{} {} {}'.format(frames[0][0][0][0], frames[0][0][0][1], frames[0][0][0][2])) # Prints 222 111 123 as expected.
# Saving.
gif = []
for f in frames:
gif.append(Image.fromarray(f))
gif[0].save('_gif.gif', save_all=True, append_images = gif[1:], loop=0)
# Now we open the new gif and check our first pixel of the first frame:
frames_new = gif_open('_gif.gif')
print('{} {} {}'.format(frames_new[0][0][0][0], frames_new[0][0][0][1], frames_new[0][0][0][2])) # Prints different values. Is this expected behavior? I'm not sure but I think the problem lies in the gif reader being unable to read RGB, reading P or L and having to convert to RGB. |
The fix is described in #3665 |
If I run the code from your last comment with hopper.gif from the test images, I get the same pixel value both times. Do you see a problem running it using https://github.com/python-pillow/Pillow/blob/master/Tests/images/hopper.gif? Are you able to upload an image that causes your script to print different values? |
@radarhere the issue does not occur with static gifs. |
Here is a simpler, Pillow-only version of the code, using one of the test images - from PIL import Image
im = Image.open('Tests/images/hopper.gif')
print(im.convert('RGB').getpixel((0, 0)))
im2 = Image.new("RGB", im.size)
im.convert('RGB').save('_gif.gif', save_all=True, append_images=[im2])
im = Image.open('_gif.gif')
print(im.convert('RGB').getpixel((0, 0))) At a basic level, here is what it is happening - the image starts out in P mode with 256 colours. You convert it to RGB, discarding the palette data. Then the image is saved, which converts it to P mode, creating a new palette of maximum 256 colours. This new palette is not exactly the same as the old one, so the colours are slightly changed. |
@radarhere so the solution is not converting to RGB? |
I'm a little limited in my response because I don't know exactly what you're after. I presume that in between reading and writing the GIF frames, you plan to transform them somehow. A fundamental problem with that vague scenario is the limitation of 256 colours in each GIF frame. If you were to read a frame and paste a new image in the middle, one with many colours in it, then there might be more than 256 colours in that new frame, which can't be perfectly represented in the GIF format. Also, be aware that as discussed in #3735, Pillow has a problem reading colours from the frames after the first frame at the moment. However, regarding the first frame, yes, not converting the image to RGB would work. This would mean that the original palette is kept, and could be saved back. Another option would be the from PIL import Image
im = Image.open('Tests/images/hopper.gif')
print(im.convert('RGB').getpixel((0, 0)))
im2 = Image.new("RGB", im.size)
im = im.convert('RGB')
im = im.quantize()
im.save('_gif.gif', save_all=True, append_images=[im2])
im = Image.open('_gif.gif')
print(im.convert('RGB').getpixel((0, 0))) |
I want to encode information in gifs by slightly changing the colors of pixels in each frame, and for that I need the values to remain all exactly the same after being edited and saved. As you suggested, the quantize method does indeed seem to work, but I'm not sure if it will work in practice where thousands of pixels are changed. I also tried not converting to RGB, it works as well, but produces a black and white gif instead of a colored gif like the original, so I can't really use that. |
Okay. I don't know why the GIF produced by not converting should be black and white, unless you're somehow losing the palette. For the record, the following code works fine for me. from PIL import Image
im = Image.open('gif.gif')
print(im.convert('RGB').getpixel((0, 0)))
im.save('_gif.gif', save_all=True)
im = Image.open('_gif.gif')
print(im.convert('RGB').getpixel((0, 0))) As an alternate, far simpler suggestion - if your goal is not actually to encode information in pixels, but just to add information to a GIF file, then you could use comments - from PIL import Image
im = Image.open('gif.gif')
im.save('_gif.gif', save_all=True, comment=b"I am a comment")
im = Image.open('_gif.gif')
print(im.info["comment"]) |
Talking about changing thousands of pixels sounds like you are trying to encode a lot of information. The limitation of 256 colours might be a problem for you, but it would naturally depend on how much information you are trying to encode, how you are doing it, and how visually similar you require the resulting image to be. I imagine you already have an algorithm in mind, but here are my thoughts on a way to do it, while keeping the GIF the same. It's not perfect, but see what you think - import math
from PIL import Image, ImageSequence, GifImagePlugin
def readImage(filename):
im = Image.open(filename)
frames = []
def appendToFrames(im):
im_copy = im.copy()
im_copy.dispose_extent = im.dispose_extent
im_copy.disposal_method = im.disposal_method
im_copy.global_palette = im.global_palette
frames.append(im_copy)
appendToFrames(im)
for i in range(1, im.n_frames):
# Instead of combining the deltas into a single image as Pillow normally would
# This clears the image data already loaded, giving us just the data from the new frame
im.im = None
im.dispose = None
im.seek(i)
appendToFrames(im)
return frames
def writeImage(filename, frames):
# Instead of saving a series of complete images, this saves the deltas
with open('_gif.gif', 'wb') as fp:
for s in GifImagePlugin._get_global_header(frames[0], frames[0].info):
fp.write(s)
for frame in frames:
dispose_extent = frame.dispose_extent
disposal_method = frame.disposal_method
include_color_table = frame.palette.palette != frame.global_palette.palette
frame = frame.crop(dispose_extent)
GifImagePlugin._write_frame_data(fp, frame, dispose_extent[:2], {
'disposal': disposal_method,
'duration': frame.info["duration"],
'include_color_table': include_color_table
})
def embedMessage(frames, message):
# Embed the message by changing pixels
# 1 changed pixel indicates the start of a letter
# Then there are 3 more pixels
# Each of the 3 is either the next most similar pixel above it in the palette,
# the next most similar below it in the palette, or unchanged
# That gives 3 possible states, stored over 3 pixels gives 3*3*3 = 27 characters, the alphabet plus a space character
def putSimilarPixel(frame, coord, state):
if state == 0:
return
pixel = frame.getpixel(coord)
palette = frame.im.getpalette("RGB")[:768]
baseColor = palette[pixel*3:pixel*3+3]
colors = []
for i in range(int(len(palette) / 3)):
colors.append(palette[i*3:i*3+3])
minDistance = None
paletteRange = range(pixel+1, int(len(palette) / 3)) if state == 1 else range(pixel)
for i in paletteRange:
color = colors[i]
distance = abs(baseColor[0] - color[0]) + \
abs(baseColor[1] - color[1]) + \
abs(baseColor[2] - color[2])
if minDistance is None or distance < minDistance:
newPixel = i
minDistance = distance
frame.putpixel(coord, newPixel)
charactersPerFrame = math.ceil(len(message) / len(frames))
for frame in frames:
states = []
for char in message[:charactersPerFrame]:
charPosition = characterDictionary.index(char)
states += [
1,
[1,0,-1][math.floor(charPosition / 9)],
[1,0,-1][math.floor(charPosition / 3 % 3)],
[1,0,-1][math.floor(charPosition % 3)]
]
message = message[charactersPerFrame:]
for y in range(frame.height):
for x in range(frame.width):
if (x+3 > frame.width or
frame.getpixel((x, y) )in [0, 255] or
frame.getpixel((x+1, y)) in [0, 255] or
frame.getpixel((x+2, y)) in [0, 255] or
frame.getpixel((x+3, y)) in [0, 255]):
continue
putSimilarPixel(frame, (x, y), states.pop(0))
if not states:
break
if not states:
break
def decodeMessage(frames, newFrames):
# Compare the changed frames with the original frames, to decode the message
message = ''
charFound = False
states = []
for i, frame in enumerate(frames):
for y in range(frame.height):
for x in range(frame.width):
oldPixel = frame.getpixel((x, y))
newPixel = newFrames[i].getpixel((x, y))
if charFound:
if newPixel == oldPixel:
state = 0
elif newPixel < oldPixel:
state = -1
else:
state = 1
states.append(state)
if len(states) == 3:
message += characterDictionary[[1,0,-1].index(states[0])*9 + [1,0,-1].index(states[1])*3 + [1,0,-1].index(states[2])]
states = []
charFound = False
elif oldPixel != newPixel:
charFound = True
return message
characterDictionary = 'abcdefghijklmnopqrstuvwxyz '
frames = readImage('gif.gif')
embedMessage(frames, 'i am encoded within the image')
writeImage('_gif.gif', frames)
frames = readImage('gif.gif')
newFrames = readImage('_gif.gif')
print(decodeMessage(frames, newFrames)) |
I open and save gifs appending frame by frame, as shown in my second post here, unlike you did. I do that because it makes for easier manipulation. Maybe that's the reason I'm losing the palette? And if so, can I save the palette somehow? |
Can you include here the exact code that's giving you the black and white images? |
Yes, this is it: #!/usr/bin/env python3
import numpy
from PIL import Image
def gif_open(filename):
image = Image.open(filename)
frames = []
try:
while True:
frames.append(numpy.array(image))
image.seek(image.tell()+1)
except EOFError:
pass
return numpy.asarray(frames)
frames = gif_open('gif.gif')
gif = []
for f in frames:
gif.append(Image.fromarray(f))
gif[0].save('_gif.gif', save_all=True, append_images = gif[1:], loop=0) |
Yes, the conversion to a numpy array does not include the palette. Storing and restoring the palette should work for you - #!/usr/bin/env python3
import numpy
from PIL import Image
def gif_open(filename):
image = Image.open(filename)
frames = []
try:
while True:
frames.append([numpy.array(image), image.getpalette()])
image.seek(image.tell()+1)
except EOFError:
pass
return numpy.asarray(frames)
frames = gif_open('gif.gif')
gif = []
for f, palette in frames:
image = Image.fromarray(f)
image.putpalette(palette)
gif.append(image)
gif[0].save('_gif.gif', save_all=True, append_images = gif[1:], loop=0) |
Works! So the only problem left is:
Hopefully a fix will be out soon. Anyway, you may close this issue now. Thanks for helping :) |
What did you do?
Edited frames of a gif and saved them.
What did you expect to happen?
Expected them to be exactly as I saved.
What actually happened?
When I reopened, the frames were not the same.
What are your OS, Python and Pillow versions?
Linux, Python3.5, Pillow 5.4.1
[operations with the array...]
The text was updated successfully, but these errors were encountered: