【问题标题】:Python tkinter displaying images as movie streamPython tkinter 将图像显示为电影流
【发布时间】:2016-10-13 11:14:26
【问题描述】:

我正在尝试像录制一样快速截屏并显示图像。除了显示窗口偶尔会出现白框“闪烁”之外,它似乎都运行良好。它似乎不是每次更新或每隔一帧,而是每 5 次左右。对原因有什么想法吗?

from tkinter import *
from PIL import Image, ImageGrab, ImageTk
import threading
from collections import deque
from io import BytesIO

class buildFrame:
    def __init__(self):
        self.root = Tk()
        self.land = Canvas(self.root, width=800, height=600)
        self.land.pack()
        self.genObj()
        self.thsObj = self.land.create_image(0,0, anchor='nw', image=self.imgObj)
        self.sStream = deque()
        self.spinning = True

        prQ = threading.Thread(target=self.procQ)
        prQ.start()

        t1 = threading.Thread(target=self.snapS, args=[100])
        t1.start()

    def genObj(self):
        tmp = Image.new('RGBA', (800, 600), color=(0, 0, 0))
        self.imgObj = ImageTk.PhotoImage(image=tmp)

    def procQ(self):
        while self.spinning == True:
            if self.sStream:
                self.land.itemconfig(self.thsObj, image=self.sStream[0])
                self.sStream.popleft()

    def snapS(self, shtCount):
        quality_val = 70

        for i in range(shtCount):
            mem_file = BytesIO()
            ImageGrab.grab().save(mem_file, format="JPEG", quality=quality_val)
            mem_file.seek(0)
            tmp = Image.open(mem_file)
            tmp.thumbnail([800, 600])
            img = ImageTk.PhotoImage(tmp)
            self.sStream.append(img)

        mem_file.close()



world = buildFrame()
world.root.mainloop()

【问题讨论】:

    标签: python tkinter pillow


    【解决方案1】:

    您应该避免在非 GUI 线程上进行 Tk 调用。如果您完全摆脱线程并使用after 来安排图像捕获,这会更加顺利。

    from tkinter import *
    from PIL import Image, ImageGrab, ImageTk
    from io import BytesIO
    
    class buildFrame:
        def __init__(self):
            self.root = Tk()
            self.land = Canvas(self.root, width=800, height=600)
            self.land.pack()
            tmp = Image.new('RGBA', (800, 600), color=(0, 0, 0))
            self.imgObj = ImageTk.PhotoImage(image=tmp)
            self.thsObj = self.land.create_image(0,0, anchor='nw', image=self.imgObj)
            self.root.after("idle", self.snapS)
    
        def snapS(self):
            quality_val = 70
            mem_file = BytesIO()
            ImageGrab.grab().save(mem_file, format="JPEG", quality=quality_val)
            mem_file.seek(0)
            tmp = Image.open(mem_file)
            tmp.thumbnail([800, 600])
            self.image = ImageTk.PhotoImage(tmp)
            self.land.itemconfig(self.thsObj, image=self.image)
            mem_file.close()
            self.root.after(10, self.snapS)
    
    world = buildFrame()
    world.root.mainloop()
    

    如果你真的想使用线程,你应该对图像流进行排队,并让 UI 线程从流中反序列化 tkinter 图像并显示它。所以一个线程进行捕获,主线程进行显示。

    编辑

    以下版本继续使用线程进行捕获并通过双端队列传递数据,但确保只有 Tk UI 线程对 Tk 对象进行操作。这需要一些工作来避免在队列中累积图像,但图像之间 100 毫秒的延迟目前可以正常工作。

    from tkinter import *
    from PIL import Image, ImageGrab, ImageTk
    import sys, threading, time
    from collections import deque
    from io import BytesIO
    
    class buildFrame:
        def __init__(self):
            self.root = Tk()
            self.root.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
            self.land = Canvas(self.root, width=800, height=600)
            self.land.pack()
            self.genObj()
            self.thsObj = self.land.create_image(0,0, anchor='nw', image=self.imgObj)
            self.sStream = deque()
            self.image_ready = threading.Event()
            self.spinning = True
    
            self.prQ = threading.Thread(target=self.procQ)
            self.prQ.start()
    
            self.t1 = threading.Thread(target=self.snapS, args=[100])
            self.t1.start()
    
        def on_destroy(self):
            self.spinning = False
            self.root.after_cancel(self.afterid)
            self.prQ.join()
            self.t1.join()
            self.root.destroy()
    
        def genObj(self):
            tmp = Image.new('RGBA', (800, 600), color=(0, 0, 0))
            self.imgObj = ImageTk.PhotoImage(image=tmp)
    
        def procQ(self):
            while self.spinning == True:
                if self.image_ready.wait(0.1):
                    print(len(self.sStream))
                    self.image_ready.clear()
                    self.afterid = self.root.after(1, self.show_image)
    
        def show_image(self):
            stream = self.sStream[0]
            self.sStream.popleft()
            tmp = Image.open(stream)
            tmp.thumbnail([800, 600])
            self.image = ImageTk.PhotoImage(tmp)
            self.land.itemconfig(self.thsObj, image=self.image)
            stream.close()
    
        def snapS(self, shtCount):
            quality_val = 70
    
            while self.spinning:
                mem_file = BytesIO()
                ImageGrab.grab().save(mem_file, format="JPEG", quality=quality_val)
                mem_file.seek(0)
                self.sStream.append(mem_file)
                self.image_ready.set()
                time.sleep(0.1)
    
    def main():
        world = buildFrame()
        world.root.mainloop()
        return 0
    
    if __name__ == '__main__':
        sys.exit(main())
    

    【讨论】:

    • “您应该将图像流排队,并让 UI 线程从流中反序列化 tkinter 图像并显示它。”这不是我在这里做的吗? self.sStream = deque() self.sStream.append(img)
    • 您说我应该避免在 tkinter 主循环线程中创建线程。有没有具体的原因?我真诚地问,因为我不知道。我这样做是因为我希望远程系统的图像在应用程序能够继续接收鼠标和键盘命令时继续。另一个线程只是检查队列中的新图像,然后弹出 [0]。我预测总共会有 4 个线程。在 gui 之上,以便远程存在。有没有更好的方法来维护与 tkinter 主循环一起运行的代码,仍然可以绘制到画布上?
    • 当你对 tkimage 进行排队时,你只是将图像的名称放到了双端队列中。您已经创建了图像本身。您可以使用self.sStream.append(mem_file) 传递图像数据。然后,阅读器线程可以发布一个 Tk 事件,让 UI 线程创建和显示 Tk 图像。查看编辑后的代码。
    猜你喜欢
    • 1970-01-01
    • 2020-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-25
    • 1970-01-01
    • 2021-02-18
    相关资源
    最近更新 更多