I am writing a simple widget to animate gifs in tkinter using PILL since tkinter does not natively support them. The issue I am having is that some gifs are flickering. Below is an example if this effect.
I am trying to animate this gif:
This gifs source can be found here.
However, when I run my code, the animation turns out like this:
I split apart the gif, and every other frame is partial like this:
After some research I believe this is a way of compressing gif files so that some frames only represent movement. I am not 100% on this however, and I could be wrong there. If that is the case, how can I reform the images in a way to recreate the quality in the original gif?
I have been able to create create a simple work around that just skips every other frame, but that does not fix the actual issue, and that most likely will not work with every gif like this.
How can I display the frames as such that the animation recreates the original quality of the gif.
Implimentation:
import tkinter as tk
from PIL import Image, ImageTk, ImageSequence
class AnimatedGif:
def __init__(self, root, src=''):
self.root = root
# Load Frames
self.image = Image.open(src)
self.frames = []
self.duration = []
for frame in ImageSequence.Iterator(self.image):
self.duration.append(frame.info['duration'])
self.frames.append(ImageTk.PhotoImage(frame))
self.counter = 0
self.image = self.frames[self.counter]
# Create Label
self.label = tk.Label(self.root, image=self.image)
self.label.pack()
# Start Animation
self.__step_frame()
def __step_frame(self):
# Update Frame
self.label.config(image=self.frames[self.counter])
self.image = self.frames[self.counter]
# Loop Counter
self.counter += 1
if self.counter >= len(self.frames):
self.counter = 0
# Queue Frame Update
self.root.after(self.duration[self.counter], lambda: self.__step_frame())
def pack(self, **kwargs):
self.label.pack(**kwargs)
def grid(self, **kwargs):
self.label.grid(**kwargs)
if __name__ in '__main__':
root = tk.Tk()
gif = AnimatedGif(root, '144.gif')
gif.pack()
root.mainloop()
The disposal method was the cause of the issue like I previously thought. The partial frames have their disposal method set to 2, and it was happening every other frame because the other frames were set to 1 like so:
1, 2, 1, 2, 1, 2, 1, 2...
Disposal method 2 uses the previous frame as a background, and changes only the opaque pixels in the current frame. Method 1 will copy the entire image over the other one while leaving the transparency intact.
iv) Disposal Method - Indicates the way in which the graphic is to
be treated after being displayed.
Values : 0 - No disposal specified. The decoder is
not required to take any action.
1 - Do not dispose. The graphic is to be left
in place.
2 - Restore to background color. The area used by the
graphic must be restored to the background color.
3 - Restore to previous. The decoder is required to
restore the area overwritten by the graphic with
what was there prior to rendering the graphic.
4-7 - To be defined.
Source: www.w3.org
In the code below, a disposal method of 1 is handles equivalently to method 0. (In that it takes just the current frame rather than pasting it onto the last frame first) It does this because I was getting the tan color on the partial frames was bleeding through, and handling it in the way it is handled in the code provides the desirable result.
Methods 3+ are omitted from this script due to them being rare and not being relevant to to this question as this gif uses methods 0 and 1.
from PIL import Image, ImageSequence
def unpack_gif(src):
# Load Gif
image = Image.open(src)
# Get frames and disposal method for each frame
frames = []
disposal = []
for gifFrame in ImageSequence.Iterator(image):
disposal.append(gifFrame.disposal_method)
frames.append(gifFrame.convert('P'))
# Loop through frames, and edit them based on their disposal method
output = []
lastFrame = None
thisFrame = None
for i, loadedFrame in enumerate(frames):
# Update thisFrame
thisFrame = loadedFrame
# If the disposal method is 2
if disposal[i] == 2:
# Check that this is not the first frame
if i != 0:
# Pastes thisFrames opaque pixels over lastFrame and appends lastFrame to output
lastFrame.paste(thisFrame, mask=thisFrame.convert('RGBA'))
output.append(lastFrame)
else:
output.append(thisFrame)
# If the disposal method is 1 or 0
elif disposal[i] == 1 or disposal[i] == 0:
# Appends thisFrame to output
output.append(thisFrame)
# If disposal method if anything other than 2, 1, or 0
else:
raise ValueError('Disposal Methods other than 2:Restore to Background, 1:Do Not Dispose, and 0:No Disposal are supported at this time')
# Update lastFrame
lastFrame = loadedFrame
return output
This script returns a list of image object that can be further modified to be used with tkinter or other GUI frameworks.