0

I try to build a GUI application to grab frames from a camera and display them in a Tkinter GUI. The Tkinter mainloop is executed in the main thread, while the frame grabbing and updating of the gui takes place in a separate thread.

The code below works as a video stream is grabbed and displayed correctly in my gui window. However when I invoke the on_close() method by clicking the "x" to close the gui, the gui will close, but the program won't terminate fully. Last CLI output will be "Mainloop stopped!", but the program does not terminate as I would expect. So, I suspect the additional thread keeps on running, even though I quit the while loop in the threads run() method via the stop event.

This is my code:

import threading    
import cv2
import tkinter
from tkinter import ttk
from PIL import Image
from PIL import ImageTk

import camera


class mainThread(threading.Thread):

    def __init__(self, gui):
        threading.Thread.__init__(self)
        self.stop_event = threading.Event()
        self.gui = gui

    def stop(self):
        print("T: Stop method called...")
        self.stop_event.set()

    def run(self):
        print("Connecting to camera...")
        cam = camera.Camera(resolution=(1280, 960), exposure=-3, bit_depth=12)
        cam.connect(device_id=0)

        while (not self.stop_event.is_set()):
            print("running..., stop event = " + str(self.stop_event.is_set()))
            # retrieve frame and frame time
            frame, _, _ = cam.get_frame()

            # display frame
            self.gui.updater(frame)

            # wait for displaying image
            cv2.waitKey(1)


class gui(object):

    def __init__(self, root):

        self.root = root

        # create and start thread running the main loop
        self.main_thread = mainThread(self)
        self.main_thread.start()

        # bind on close callback that executes on close of the main window
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

        # change window title
        self.root.title("MCT Laser Welding Quality Control")

        # set min size of root window and make window non resizable
        self.root.minsize(600, 400)
        self.root.resizable(False, False)

        # configure grid layout
        self.root.rowconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=1)
        self.root.columnconfigure(0, weight=1)

        # create image panel
        self.image_panel = None

    def on_close(self):
        self.main_thread.stop()
        self.root.destroy()

    def updater(self, image):

        # TODO: resize frame first

        # convert image to tkinter image format
        image = Image.fromarray(image)
        image = ImageTk.PhotoImage(image)

        # if the panel does not exist, create and pack it
        if self.image_panel is None:

            # show the image in the panel
            self.image_panel = ttk.Label(self.root, image=image)
            self.image_panel.image = image  # keep reference

            # pack object into grid
            self.image_panel.grid(row=0, column=0)

        # just update the image on the panel
        else:
            self.image_panel.configure(image=image)
            self.image_panel.image = image  # keep reference

    def run(self):
        self.root.mainloop()


if __name__ == "__main__":

    # create a main window
    root = tkinter.Tk()

    # set style
    style = ttk.Style()
    style.theme_use("vista")

    # create gui instance
    user_interface = gui(root)

    # run the user interface
    root.mainloop()

    print("Mainloop stopped!")

EDIT: I found out, that the line

image = ImageTk.PhotoImage(image)

is preventing the thread from stopping as described above. When I remove this line and simply update the label text with an incrementing number, everything works as excepted and the thread terminates, when I close the gui window. Any ideas, why ImageTk.PhotoImage() causes the thread to not terminate properly?

  • The main weakness of your code in the fact that your `tk`-related commands casts from two threads, when they needs to be originate from one. You create `root` in real main thread, but operate with `root` in your "fake" one (you even has a second `mainloop` here, thank God it don't executes). Before asking such question I suggest to fix all that stuff, keep in mind that `tkinter` isn't a thread safe and anything can tear apart, if you trying to break rules. Make a `Queue`, fill it with frames in "fake" main thread, and check that `Queue` from real one. Don't make things more complicated! – CommonSense Sep 22 '17 at 07:27
  • You can even create two `Queues`, and fill one with tasks `get_one_frame` or something similar, and feed it to thread, while grabbing frames from second. When you decide to quit - just stop populate the first one `Queue` with tasks. And when thread finds that there's no task to execute - close it. More universal, a littlebit more complicated, but much clearer! Also, is there's any point in threads? `cv2.VideoCapture()` interface can handle stream of frames without threading, just grab one frame at a time with [`after`](http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.after-method)! – CommonSense Sep 22 '17 at 07:55
  • The problem is, the code above is just a small part of the actual software. I need a separate thread to run a loop forever in order to do all the other stuff like grabbing frames from the camera putting them in a queue, processing the frames from the queue in a pool of parallel processes, reading and writing from a remote and a local database and listening to incoming signals from a MQTT connection. All of this is currently running in a while loop in my main thread. I need to shift that into a separate thread and run the GUI in the main thread instead. – LB-Projects Sep 22 '17 at 09:18
  • I guess I need to pass every signal between GUI and the thread running the loop via queues, is it? I'm not very familiar with Tkinter, so I'm not quite sure, how you would usually build a Tkinter app. But anyway, thanks for your advice. I'll figure out, how to improve the architecture of the program. – LB-Projects Sep 22 '17 at 09:20
  • But I only assume, that bad design is a cause. You already pointed on the line, that corrupt your logic. But try to look further. Your `image` is a child attribute of `image_panel`, while `image_panel` has not one (self), but two parents (self and self.root) from different threads! Does it's looks ok for you? I know, it doesn't. Maybe it's works with simple increment integer because there's no such nested corrupt reference between threads. I can't test it by myself, but I wonder how your snippet would work if you store `image_panel` as `self.root.image_panel`. I fear, that it runs in same way. – CommonSense Sep 22 '17 at 09:44
  • And yes, queues is a good interface between threads and GUI, you can create a `Queue` for each part of your programm that needs to be represented on GUI, and check those queues continuously with [`after`](http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.after-method) loop from GUI. And if there's some "signal" or a "frame" - act accordingly! There's many examples how such interface can be implemented on the internet ([one of them](https://stackoverflow.com/questions/16745507/tkinter-how-to-use-threads-to-preventing-main-event-loop-from-freezing)). – CommonSense Sep 22 '17 at 09:52
  • Thanks for your help, CommonSense! I'm now using a queue to pass frames from the background thread to the main thread. However I'm not using the polling technique via after, but instead bind an event and call that event via generate_event() in the background thread, whenever a frame is put() into the queue. That prevents unneccessary calls of the callback method as it would be the casse with the polling technique. – LB-Projects Sep 25 '17 at 07:40
  • The only concern I have is, that I have to pass the root object to the background thread in order to access the generate_event() method. Is that okay? I'm not directly modifying widget in the bg thread any more. – LB-Projects Sep 25 '17 at 07:40
  • Ask yourself the same question. I'm not your mentor nor employer, hence if solution works and all of your colleagues are happy with it - it's really okay. If you ask me personally, I prefer "polling technique via after", it's clearer in my opinion. I don't know why you rejected it, maybe because you think that this solution is unoptimal, but that's not so true statement, the difference between these two options is insignificant. Also, most of synchronization processes between threads implemented in that same way under the hood of threading module. But as I said - it's up to you to decide (: – CommonSense Sep 25 '17 at 08:51
  • But I understand and your point too. The only thing that I worry about - how it's really works. If you generate an event, do you just put your "event" in a queue of `tkinter`'s event loop, or all stuff proceeded in thread, where event has been generated. I'm lack of knowledge here, but if it's works without any barking at you - it's reliable. You can test it by yourself, in which thread callbacks executes. I think, that it works as expected (first case), but it's only assumptions! [More about it you can find here if you wish](http://wiki.tcl.tk/1527). – CommonSense Sep 25 '17 at 09:11

1 Answers1

0

You need to join the thread to wait for it to close.

def stop(self):
    print("T: Stop method called...")
    self.stop_event.set()
    self.join()

Also, you may need to close the open connection to the camera.

while (not self.stop_event.is_set()):
    ...
cam.close() # not sure about exact API here
Oluwafemi Sule
  • 36,144
  • 1
  • 56
  • 81
  • Thanks for your answer! But this just blocks the main_thread forever, after hitting "x" on the window. I also found out, that after calling main_thread.stop() the thread is still alive. Even if I check again (via .is_alive()) after ten seconds, the thread is still alive, however the while loop is not executed any more. I guess, this is the reason, why the program does not terminate. So, how do I stop the thread? It should be done automatically by the OS, isn't it? – LB-Projects Sep 22 '17 at 05:16