2013-06-20 75 views
2

我正在試圖製作一個程序來顯示使用Tkinter的動畫GIF。下面是代碼,我最初使用:在Tkinter中使用PIL顯示動畫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:

enter image description here

這工作得很好,但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) 

但是,這會偶爾閃爍一張圖片。雖然這張照片看起來不錯,但作爲一個程序來說它是無用的。我究竟做錯了什麼?

+0

要做的第一件事就是找出問題所在。嘗試每次通過循環將'temp'保存到新的.PNG文件中。他們已經搞砸了嗎?如果是這樣,Tkinter與這個問題無關,這意味着你可以寫一個更小的[SSCCE](http://sscce.org)。 – abarnert

+0

更仔細地查看了代碼,併爲您完成了建議的測試:問題完全在於您的PIL代碼中;試圖解決Tkinter的問題就是在錯誤的樹上追逐一隻野鵝。 – abarnert

+0

相關:[用tkinter播放動畫GIF](https://stackoverflow.com/q/43770847/190597) – unutbu

回答

9

其中之一,你正在爲每一幀創建一個新的畫布對象。最終你會有成千上萬的圖像堆疊在一起。這是非常低效的;當您開始擁有數千個對象時,畫布窗口小部件會出現性能問題。

不是在畫布上創建新的圖像對象,而是使用畫布的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控件降低複雜性多一點。

4

你的問題與Tkinter無關。 (據我所知,你可以有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,已經搞砸了,沒有傳統知識相關的代碼被調用。

事實上,你甚至可以更方便地測試同樣的事情:

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

的問題是,你從幀#0從粘貼幀#1像素以上的像素頂部,但保留幀#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] 
+0

誰低估了,小心解釋爲什麼?這就解釋了爲什麼OP的代碼不起作用,以及如何解決它。當然,以一種更智能的方式重寫整個事情,同時不會犯同樣的錯誤也能解決問題,但它不能解釋OP做錯了什麼。 – abarnert