[Python] Tkinter image label performance issues when updating large image

Discussão em 'Python' iniciado por Stack, Junho 24, 2025.

  1. Stack

    Stack Membro Participativo

    This is a follow up question from Calling ImageTk.PhotoImage() in a thread causing deadlock

    I have so far fixed the deadlock issue following the design in the accepted answer. Now I am working on a responsive ui that allows user to resize the window. As the window size changes, the image shown in the label should also resize to fill the window. However, I found large images can significantly impact the frame rate. Displaying and updating images on a 4K monitor causes the UI to become inresponsive.

    I timed the set_image method, which is called recursively with root.after and found that root.after actually delays behind significantly when showing large images:

    Average delay time: 0.0014089000100102568, image size: 100 x 100
    Average delay time: 0.001673793329991895, image size: 200 x 200
    Average delay time: 0.0033536416499555344, image size: 400 x 400
    Average delay time: 0.02414946045006218, image size: 800 x 800
    Average delay time: 0.07494734207000875, image size: 1600 x 1600
    Average delay time: 0.1494195945899719, image size: 3200 x 3200


    I am using a MacBook M3 Pro so it is not a terrible machine, similar results are obtained from an i7 machine also.

    Is there a better way to display images in a Tkinter UI? Am I reaching the limitation of python and should consider C++ at this point instead?

    Below is a minimum viable code for you test.

    import tkinter as tk
    from PIL import Image, ImageTk
    import threading
    import time


    ITERATION = 20

    class Timer:
    def __init__(self):
    self.reset()

    def reset(self):
    self.time_accumulated = 0
    self.start_time = None
    self.count = 0

    def begin(self):
    self.start_time = time.perf_counter()

    def stop(self):
    if self.start_time is None:
    return
    self.time_accumulated += time.perf_counter() - self.start_time
    self.count += 1

    def get_average_time(self):
    return self.time_accumulated / self.count

    class App:
    def __init__(self, root):
    self.root = root
    self.label = tk.Label(root)
    self.label.pack()

    self.running = True
    self.frame = None
    self.image_size = 100

    # run in other thread
    self.display_thread = threading.Thread(target=self.update_image_loop, daemon=True)
    self.display_thread.start()

    self.root.protocol("WM_DELETE_WINDOW", self.on_close)
    self.loop_time_previous = time.perf_counter()

    self.timer = Timer()
    self.root.after(1, self.set_image)


    def update_image_loop(self):
    """Run in other thread."""

    while self.running:
    # self.frame = Image.open(self.image_path)
    # make a small black image
    self.frame = Image.new('RGB', (self.image_size, self.image_size), (0, 0, 0))
    time.sleep(0.001)

    def set_image(self):
    """Run in main thread."""
    self.timer.stop()
    if self.frame:
    self.image = ImageTk.PhotoImage(image=self.frame)
    self.label.config(image=self.image)
    if self.timer.count >= ITERATION:
    self.next()
    if self.running:
    self.timer.begin()
    self.root.after(1, self.set_image)

    def next(self):
    print(f'Average delay time: {self.timer.get_average_time()}, image size: {self.image_size} x {self.image_size}')
    self.image_size *= 2
    if self.image_size >= 5000:
    self.root.destroy()
    return
    self.timer.reset()

    def on_close(self):
    self.running = False
    self.display_thread.join()
    self.root.destroy()

    if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

    Continue reading...

Compartilhe esta Página