pythontkinterflicker

Python tkinter label is flickering while frequently changing displayed image


I currently write a Python program that is used to control a camera in a laboratory. I use tkinter for the GUI and the label that displays the image of the camera (10 FPS, but could be changed) changes to its background color for a fraction of a second while the new image is given to it. The occurance of the flickering does not happen at a set frequency and doesn't change with FPS. It flickers between 3 and 5 times per second. Since the window holds additional GUI elements that have to be accessible during the live stream, I gave the task of refreshing the image to a different thread. There are 3-4 parallel threads for similar tasks running, depending on the state of the program. Down below there are some relevant parts of the code but I can't give you a real MRE since the software for the camera is missing:

import tkinter as tk
from tkinter import *
from tkinter import ttk
import threading
import time

import imagePreprocessing

#Thread function to update the displayed picture according to the FPS defined above
def liveFeedThread():
    global timestamp, labelImageFeed
    while True:
        currentTime = time.perf_counter_ns() #gets current time in nano seconds
        if currentTime - timestamp > 1000000000/fps: #one second/fps -> time between pictures
            image = imagePreprocessing.takeImage() #different module controlling the camera, images are always 640x480
            timestamp = time.perf_counter_ns() #update timestamp before processing picture to ensure timing
            labelImageFeed['image'] = image
        else:
            time.sleep(1E-2)

#Window
window = tk.Tk()
window.resizable(False, False)
window.geometry('640x580') #640x480 for downscaled livestream and 100px height for window components

frameTop = ttk.Frame(window, width=640, height=480)
frameTop.pack(side=TOP, fill=BOTH)

labelImageFeed = tk.Label(frameTop, width=640, height=480, bg='black')
labelImageFeed.pack(expand=True, fill=BOTH)

frameBottom = ttk.Frame(window, width=640, height=100, padding=10)
frameBottom.pack(side=BOTTOM, fill=BOTH)
frameBottom.columnconfigure(0, weight=10)
frameBottom.columnconfigure(2, weight=10)
frameBottom.columnconfigure(4, weight=10)
frameBottom.columnconfigure(6, weight=10)
frameBottom.rowconfigure(0, weight=10)
frameBottom.rowconfigure(1, weight=10)
frameBottom.rowconfigure(2, weight=10)
#more code for the GUI elements in the bottom frame is left out of this example

#Live-feed
liveFeed = threading.Thread(target=liveFeedThread, daemon=True)
liveFeed.start()

#more code for different threads is also left out

window.mainloop()

I searched for the problem and found someone telling me that tkinter doesn't like threading. Since I can't let all tasks run in the main thread, I hope it is not that. I tried to save the picture in a global variable to ensure it is not only saved in the label. I changed the label to a canvas. I used different FPS. I have a different, older program that uses the same camera but doesn't have any other GUI elements. In this program there are no flickers, so I am pretty sure the problem is about something I don't know about layout managing of tkinter or Python in general since I normally code in Java/C++.


Solution

  • I think we can solve the problem with "queue" and "threading" modules. Once I modified your code arbitrarily. First, import the required modules.

    import time
    import tkinter as tk
    from tkinter import ttk
    from threading import Thread
    from queue import Queue
    
    import imagePreprocessing
    

    And create gui elements and variables.(convert to class)

    class CamWindow(tk.Tk):
        def __init__(self):
            super().__init__()
            self.resizable(False, False)
            self.geometry('640x580')
            self.frameTop = ttk.Frame(self, width=640, height=480)
            self.frameTop.pack(side=tk.TOP, fill=tk.BOTH)
    
            self.labelImageFeed = tk.Label(self.frameTop, width=640, height=480, bg='black')
            self.labelImageFeed.pack(expand=True, fill=tk.BOTH)
    
            self.frameBottom = ttk.Frame(self, width=640, height=100, padding=10)
            self.frameBottom.pack(side=tk.BOTTOM, fill=tk.BOTH)
            self.frameBottom.columnconfigure(0, weight=10)
            self.frameBottom.columnconfigure(2, weight=10)
            self.frameBottom.columnconfigure(4, weight=10)
            self.frameBottom.columnconfigure(6, weight=10)
            self.frameBottom.rowconfigure(0, weight=10)
            self.frameBottom.rowconfigure(1, weight=10)
            self.frameBottom.rowconfigure(2, weight=10)
    
            self.timestamp = time.perf_counter_ns()
            self.fps = 10
            self.camImage = Queue()
            self.t1 = Thread(target=self.liveFeedThread)
            self.t1.start()
            self.image_update()
    

    Here, we determine the initial values ​​of self.timestamp and self.fps, and declare a queue called self.camImage.

    And now, we define a function to put the processed image into a queue and a function to update the queued image to the GUI.

        def liveFeedThread(self):
            while True:
                currentTime = time.perf_counter_ns()  # gets current time in nano seconds
                if currentTime - self.timestamp > 1000000000 / self.fps:  # one second/fps -> time between pictures
                    # image = tk.PhotoImage(file='screenshot.png')
                    image = imagePreprocessing.takeImage()  # different module controlling the camera, images are always 640x480
                    self.camImage.put(image)
                    self.timestamp = time.perf_counter_ns()  # update timestamp before processing picture to ensure timing
                else:
                    time.sleep(1E-2)
    
        def image_update(self):
            try:
                self.labelImageFeed['image'] = self.camImage.get_nowait()
            except:
                pass
            self.after(1, self.image_update)
    

    and,

    if __name__ == "__main__":
        app = CamWindow()
        app.mainloop()
    

    I'm not sure since I haven't seen the imagePreprocessing module, but I hope this solves your problem.