pythontkinterpython-imaging-library

Calling ImageTk.PhotoImage() in a thread causing deadlock


I am working on a camera applicaiton where I need to get images from camera buffer and display it in a TK label live:

def display_thread_run(self):
    while self.is_acquiring:
        image_data = self.camera.get_next_image()
        # --- some image conversion methods omitted ---
        pil_image = Image.fromarray(image_data)
        photo = ImageTk.PhotoImage(image=pil_image)
        self.image_frame.after(0, self._create_photo_and_update_gui, photo)
        time.sleep(0.01)

When the application exits, I need to close the camera stream, join the thread and at the very end, release camera related resources:

def stop_and_cleanup(self):
    camera.stop_acquisition()
    self.is_acquiring = False
    if self.display_thread and self.display_thread.is_alive():
        self.display_thread.join()
    camera.release()
    logger.info("Display resources released")

However, after I click the close window button (cross button), photo = ImageTk.PhotoImage(image=pil_image) causes deadlock and the thread won't join.

Setting thread.daemon=True and removing display_thread.join() won't work because camera.release() must be called after the thread is properly cleaned.

I'm using:

A minimum code example:

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


IMG_PATH = 'test_img.jpg'

class App:
    def __init__(self, root):
        self.root = root
        self.label = tk.Label(root)
        self.label.pack()
        self.running = True
        self.img = Image.open(IMG_PATH)
        
        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)

    def update_image_loop(self):
        while self.running:
            tk_img = ImageTk.PhotoImage(image=self.img)
            self.label.after(0, self.set_image, tk_img)
            time.sleep(0.001)

    def set_image(self, tk_img):
        self.label.img = tk_img  # keep reference
        self.label.config(image=tk_img)

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

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

Solution

  • tkinter is not thread-safe and it can make problem.

    It works for me if I remove after() from thread and run directly set_image() but this make fickering image.

    So I would use thread only to get (and process) image from camera,

    def update_image_loop(self):
        """Run in other thread."""
    
        while self.running:
            self.frame = Image.open(IMG_PATH)
            #self.frame = ... get image from camera ...
            #... processing image ...
            time.sleep(0.001)
    

    and in main thread I would use after() to replace image on canvas.

    def __init__(self, root):
    
        # ... code ...
    
        # start in main thread (after 1ms)
        self.root.after(1, self.set_image)
    
    def set_image(self):
        """Run in main thread."""
    
        self.image = ImageTk.PhotoImage(image=self.frame)
        self.label.config(image=self.image)
    
        if self.running:
            self.root.after(1, self.set_image)  # repeat after 1ms
    

    As for me it works fast enough.


    My code used for tests:

    I used image Lenna from Wikipedia

    import tkinter as tk
    from PIL import Image, ImageTk
    import threading
    import time
    
    
    IMG_PATH = 'test_img.jpg'
    IMG_PATH = 'lenna.png'
    
    class App:
        def __init__(self, root):
            self.root = root
            self.label = tk.Label(root)
            self.label.pack()
    
            self.running = True
    
            # 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)
    
            # start in main thread (after 1ms)
            self.root.after(1, self.set_image)
    
        def update_image_loop(self):
            """Run in other thread."""
    
            while self.running:
                #print('[DEBUG] get image')
                self.frame = Image.open(IMG_PATH)
                #self.frame = ... get image from camera ...
                #... processing image ...
                #print('[DEBUG] sleep')
                time.sleep(0.001)
            #print('[DEBUG] exit loop')
    
        def set_image(self):
            """Run in main thread."""
    
            #print('[DEBUG] set_image')
            self.image = ImageTk.PhotoImage(image=self.frame)
            self.label.config(image=self.image)
            #self.label['image'] = self.img
            if self.running:
                self.root.after(1, self.set_image)  # repeat after 1ms
    
        def on_close(self):
            #print("[DEBUG] on_close")
            self.running = False
            #print('[DEBUG] join')
            self.display_thread.join()
            #print('[DEBUG] destroy')
            self.root.destroy()
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = App(root)
        root.mainloop()