Displaying animated GIFs in Tkinter using PIL

12,002

Solution 1

For one, you are creating a new canvas object for every frame. Eventually you will have thousands of images stacked on top of one another. This is highly inefficient; the canvas widget has performance issues when you start to have thousands of objects.

Instead of creating new image objects on the canvas, just reconfigure the existing object with the itemconfig method of the canvas.

Second, you don't need the complexities of threading for such a simple task. There is a well known pattern in tkinter for doing animations: draw a frame, then have that function use after to call itself in the future.

Something like this:

def animate(self):
    if self._image_id is None:
        self._image_id = self.display.create_image(...)
    else:
        self.itemconfig(self._image_id, image= the_new_image)
    self.display.after(self.gif["delay"], self.animate)

Finally, unless there's a strict reason to use a canvas, you can lower the complexity a little more by using a Label widget.

Solution 2

Your problem has nothing to do with Tkinter. (For all I know, you may also have Tk problems, but your images are already bad before you get to Tk.)

The way I tested this was to modify your anim_gif function to write out the frames as separate image file, by changing the for item in pics[1:] loop like this:

 for i, item in enumerate(pics[1:]):
    temp.paste(item)
    temp.save('temp{}.png'.format(i))
    gif['frames'].append(ImageTk.PhotoImage(temp.convert('RGBA')))

The very first file, temp0.png, is already screwed up, with no Tk-related code being called.

In fact, you can test the same thing even more easily:

from PIL import Image
im = Image.open('test.gif')
temp = im.copy()
im.seek(1)
temp.paste(im.copy())
temp.save('test.png')

The problem is that you're pasting the pixels from frame #1 over top of the pixels from frame #0, but leaving the color palette from frame #0.

There are two easy ways to solve this.

First, use the RGBA-converted frames instead of the palette-color frames:

temp = pics[0].convert('RGBA')
gif['frames'] = [ImageTk.PhotoImage(temp)]
for item in pics[1:]:
    frame = item.convert('RGBA')
    temp.paste(frame)
    gif['frames'].append(ImageTk.PhotoImage(temp))

Second, don't use copy and paste at all; just copy over each frame as an independent image:

gif['frames'] = [ImageTk.PhotoImage(frame.convert('RGBA')) for frame in pics]
Share:
12,002
DoctorSelar
Author by

DoctorSelar

Updated on June 04, 2022

Comments

  • DoctorSelar
    DoctorSelar almost 2 years

    I'm trying to make a program to display an animated GIF using Tkinter. Here is the code that I originally used:

    from __future__ import division # Just because division doesn't work right in 2.7.4
    from Tkinter import *
    from PIL import Image,ImageTk
    import threading
    from time import sleep
    
    def anim_gif(name):
        ## Returns { 'frames', 'delay', 'loc', 'len' }
        im = Image.open(name)
        gif = { 'frames': [],
                'delay': 100,
                'loc' : 0,
                'len' : 0 }
        pics = []
        try:
            while True:
                pics.append(im.copy())
                im.seek(len(pics))
        except EOFError: pass
    
        temp = pics[0].convert('RGBA')
        gif['frames'] = [ImageTk.PhotoImage(temp)]
        temp = pics[0]
        for item in pics[1:]:
            temp.paste(item)
            gif['frames'].append(ImageTk.PhotoImage(temp.convert('RGBA')))
    
        try: gif['delay'] = im.info['duration']
        except: pass
        gif['len'] = len(gif['frames'])
        return gif
    
    def ratio(a,b):
        if b < a: d,c = a,b
        else: c,d = a,b
        if b == a: return 1,1
        for i in reversed(xrange(2,int(round(a / 2)))):
            if a % i == 0 and b % i == 0:
                a /= i
                b /= i
        return (int(a),int(b))
    
    class App(Frame):
        def show(self,image=None,event=None):
            self.display.create_image((0,0),anchor=NW,image=image)   
    
        def animate(self,event=None):
            self.show(image=self.gif['frames'][self.gif['loc']])
            self.gif['loc'] += 1
            if self.gif['loc'] == self.gif['len']:
                self.gif['loc'] = 0
            if self.cont:
                threading.Timer((self.gif['delay'] / 1000),self.animate).start()
    
        def kill(self,event=None):
            self.cont = False
            sleep(0.1)
            self.quit()
    
        def __init__(self,master):
            Frame.__init__(self,master)
            self.grid(row=0,sticky=N+E+S+W)
            self.rowconfigure(1,weight=2)
            self.rowconfigure(3,weight=1)
            self.columnconfigure(0,weight=1)
            self.title = Label(self,text='No title')
            self.title.grid(row=0,sticky=E+W)
            self.display = Canvas(self)
            self.display.grid(row=1,sticky=N+E+S+W)
            self.user = Label(self,text='Posted by No Username')
            self.user.grid(row=2,sticky=E+W)
            self.comment = Text(self,height=4,width=40,state=DISABLED)
            self.comment.grid(row=3,sticky=N+E+S+W)
            self.cont = True
            self.gif = anim_gif('test.gif')
            self.animate()
    
            root.protocol("WM_DELETE_WINDOW",self.kill)
    
    
    root = Tk()
    root.rowconfigure(0,weight=1)
    root.columnconfigure(0,weight=1)
    app = App(root)
    app.mainloop()
    
    try: root.destroy()
    except: pass
    

    test.gif is the following GIF:

    enter image description here

    This works fine, but the GIF quality is terrible. I tried changing it to what follows:

    def anim_gif(name):
        ## Returns { 'frames', 'delay', 'loc', 'len' }
        im = Image.open(name)
        gif = { 'frames': [],
                'delay': 100,
                'loc' : 0,
                'len' : 0 }
        pics = []
        try:
            while True:
                gif['frames'].append(im.copy())
                im.seek(len(gif['frames']))
        except EOFError: pass
    
        try: gif['delay'] = im.info['duration']
        except: pass
        gif['len'] = len(gif['frames'])
        return gif
    
    class App(Frame):
        def show(self,image=None,event=None):
            can_w = self.display['width']
            can_h = self.display['height']
    
            pic_w,pic_h = image.size
            rat_w,rat_h = ratio(pic_w,pic_h)
    
            while pic_w > int(can_w) or pic_h > int(can_h):
                pic_w -= rat_w
                pic_h -= rat_h
    
            resized = image.resize((pic_w,pic_h))
            resized = ImageTk.PhotoImage(resized)
            self.display.create_image((0,0),anchor=NW,image=resized)   
    

    However, this will occasionally flash a picture. While the picture looks good, it's pretty useless as a program. What am I doing wrong?