【问题标题】:Displaying animated GIFs in Tkinter using PIL使用 PIL 在 Tkinter 中显示动画 GIF
【发布时间】:2013-06-20 21:10:44
【问题描述】:

我正在尝试制作一个使用 Tkinter 显示动画 GIF 的程序。这是我最初使用的代码:

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 是以下 GIF:

这很好用,但 GIF 质量很差。我尝试将其更改为以下内容:

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)   

不过,这偶尔会闪现一张图片。虽然图片看起来不错,但它作为一个程序毫无用处。我究竟做错了什么?

【问题讨论】:

  • 首先要做的是找出问题所在。尝试每次通过循环将temp 保存到一个新的 .PNG 文件中。他们已经搞砸了吗?如果是这样,Tkinter 与问题无关,这意味着你可以写一个小得多的SSCCE
  • 更仔细地查看了代码,并为您完成了建议的测试:问题完全出在您的 PIL 代码中;试图修复 Tkinter 的东西是在追错树上的野鹅。

标签: python tkinter python-imaging-library animated-gif


【解决方案1】:

首先,您正在为每一帧创建一个新的画布对象。最终,您将拥有成千上万张堆叠在一起的图像。这是非常低效的;当您开始拥有数千个对象时,画布小部件会出现性能问题。

不用在画布上创建新的图像对象,只需使用画布的itemconfig 方法重新配置现有对象即可。

其次,对于这样一个简单的任务,您不需要线程的复杂性。 tkinter 中有一个众所周知的动画模式:画一个框架,然后让该函数使用after 来调用它自己。

类似这样的:

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)

最后,除非有严格的理由使用画布,否则您可以通过使用 Label 小部件进一步降低复杂性。

【讨论】:

    【解决方案2】:

    您的问题与 Tkinter 无关。 (据我所知,您可能有 Tk 问题,但在您到达 Tk 之前,您的图像已经很糟糕了。)

    我测试这个的方法是修改你的anim_gif函数,通过改变for item in pics[1:]这样的循环将帧写成单独的图像文件:

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

    第一个文件 temp0.png 已经搞砸了,没有调用任何与 Tk 相关的代码。

    事实上,您可以更轻松地测试相同的东西:

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

    问题是您将第 1 帧的像素粘贴到第 0 帧的像素之上,但保留了第 0 帧的调色板。

    有两种简单的方法可以解决这个问题。

    首先,使用 RGBA 转换的帧而不是调色板颜色的帧:

    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))
    

    第二,根本不要使用复制粘贴;只需将每一帧复制为独立图像:

    gif['frames'] = [ImageTk.PhotoImage(frame.convert('RGBA')) for frame in pics]
    

    【讨论】:

    • 谁投了反对票,想解释一下原因吗?这解释了为什么 OP 的代码不起作用,以及如何修复它。当然,以更聪明的方式重写整个事情,同时不犯同样的错误也可以解决问题,但这并不能解释 OP 做错了什么。
    猜你喜欢
    • 2017-12-03
    • 2011-02-25
    • 2017-10-01
    • 2012-09-15
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    • 2022-06-17
    • 2017-12-29
    相关资源
    最近更新 更多