Displaying animated GIFs in Tkinter using PIL

2020-02-14 08:16发布

问题:

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:

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?

回答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.



回答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]