Reputation: 31
What I want to do
Create the main window with two buttons "start button" and "stop button"
When "start button" is pressed, images of the connected USB camera are displayed in the main window
Press the "stop button" to erase the image of the USB camera displayed in [2] (leaving the main window)
Trouble
[1] and [2] was done. However, it is impossible to erase the image of the USB camera with [3]. Error message:
Exception in thread Thread-8:
Traceback (most recent call last):
File "C:\Users\usr\Anaconda3\lib\threading.py", line 916, in _bootstrap_inner self.run()
File "C:\Users\usr\Anaconda3\lib\threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
TypeError: destroy() missing 1 required positional argument: 'panel'
Code
import cv2
from PIL import Image
from PIL import ImageTk
import threading
import tkinter as tk
def button1_clicked():
thread = threading.Thread(target=videoLoop, args=())
thread.start()
def button2_clicked():
thread = threading.Thread(target=destroy, args=())
thread.start()
def destroy(panel):
panel.destroy()
def videoLoop(mirror=False):
No=0
cap = cv2.VideoCapture(No)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
while True:
ret, to_draw = cap.read()
if mirror is True:
to_draw = to_draw[:,::-1]
image = cv2.cvtColor(to_draw, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
panel = tk.Label(image=image)
panel.image = image
panel.place(x=50, y=50)
return panel
root = tk.Tk()
root.geometry("1920x1080+0+0")
button1 = tk.Button(root, text="start", bg="#fff", font=("",50), command=button1_clicked)
button1.place(x=1000, y=100, width=400, height=250)
button2 = tk.Button(root, text="stop", bg="#fff", font=("",50), command=button2_clicked)
button2.place(x=1000, y=360, width=400, height=250)
root.mainloop()
Upvotes: 3
Views: 4290
Reputation: 33193
There are some fundamental issues with what you are doing here. The basic premise is incorrect in that you should not continually destroy and re-create the Label widget to display the image. Instead just update the image that is attached to the existing widget by calling its configure() method with the new image. This is a performance fix regardless of the threading problems you have here. In general create the widgets once and update them. This avoids a cascade of geometry change events that occur as you remove and add widgets from the UI tree.
The threading design is incorrect here. You should not make Tk calls from worker threads. Tk is tied to a single thread and only events should pass between threads. To show how this might be better constructed I've modified the code to use a queue.Queue()
for passing the image frame from the opencv reader thread to the Tk thread. We can post a custom event to notify the UI there is a new frame ready (<<MessageGenerated>>
).
A last gotcha is that you should hold a reference to an image that you add to a Tk label otherwise it can get garbage collected when you don't expect it. Hence we update the self.photo
member with each new image.
import sys
import cv2
import threading
import tkinter as tk
import tkinter.ttk as ttk
from queue import Queue
from PIL import Image
from PIL import ImageTk
class App(tk.Frame):
def __init__(self, parent, title):
tk.Frame.__init__(self, parent)
self.is_running = False
self.thread = None
self.queue = Queue()
self.photo = ImageTk.PhotoImage(Image.new("RGB", (800, 600), "white"))
parent.wm_withdraw()
parent.wm_title(title)
self.create_ui()
self.grid(sticky=tk.NSEW)
self.bind('<<MessageGenerated>>', self.on_next_frame)
parent.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
parent.grid_rowconfigure(0, weight = 1)
parent.grid_columnconfigure(0, weight = 1)
parent.wm_deiconify()
def create_ui(self):
self.button_frame = ttk.Frame(self)
self.stop_button = ttk.Button(self.button_frame, text="Stop", command=self.stop)
self.stop_button.pack(side=tk.RIGHT)
self.start_button = ttk.Button(self.button_frame, text="Start", command=self.start)
self.start_button.pack(side=tk.RIGHT)
self.view = ttk.Label(self, image=self.photo)
self.view.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X, expand=True)
def on_destroy(self):
self.stop()
self.after(20)
if self.thread is not None:
self.thread.join(0.2)
self.winfo_toplevel().destroy()
def start(self):
self.is_running = True
self.thread = threading.Thread(target=self.videoLoop, args=())
self.thread.daemon = True
self.thread.start()
def stop(self):
self.is_running = False
def videoLoop(self, mirror=False):
No=0
cap = cv2.VideoCapture(No)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
while self.is_running:
ret, to_draw = cap.read()
if mirror is True:
to_draw = to_draw[:,::-1]
image = cv2.cvtColor(to_draw, cv2.COLOR_BGR2RGB)
self.queue.put(image)
self.event_generate('<<MessageGenerated>>')
def on_next_frame(self, eventargs):
if not self.queue.empty():
image = self.queue.get()
image = Image.fromarray(image)
self.photo = ImageTk.PhotoImage(image)
self.view.configure(image=self.photo)
def main(args):
root = tk.Tk()
app = App(root, "OpenCV Image Viewer")
root.mainloop()
if __name__ == '__main__':
sys.exit(main(sys.argv))
I should add that at this point if you want to show a blank image after the stop button has been pressed you can set the viewing label widget to a new blank image as shown in the constructor.
Upvotes: 1
Reputation:
Error you got actually tells what exactly is wrong with the code. TypeError: destroy() missing 1 required positional argument: 'panel'
literally says that you have to pass argument panel
to function destroy()
. You call the function implicitly with thread.start()
in the button2_clicked()
's suite. To fix the problem you should modify thread object creation:
thread = threading.Thread(target=destroy, args=(panel,))
Also you should pass panel
to button2_clicked()
function. Here another problem arises since panel
is returned by the function videoloop()
. So panel
is never returned because videoloop()
contains infinite while loop. To solve this problem you need a way to pass data between operational pecies of your code. For example, you may do it like this (simple but not robust approach):
import cv2
from PIL import Image
from PIL import ImageTk
import threading
import tkinter as tk
def button1_clicked(videoloop_stop):
threading.Thread(target=videoLoop, args=(videoloop_stop,)).start()
def button2_clicked(videoloop_stop):
videoloop_stop[0] = True
def videoLoop(mirror=False):
No = 0
cap = cv2.VideoCapture(No)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 800)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 600)
while True:
ret, to_draw = cap.read()
if mirror is True:
to_draw = to_draw[:, ::-1]
image = cv2.cvtColor(to_draw, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
image = ImageTk.PhotoImage(image)
panel = tk.Label(image=image)
panel.image = image
panel.place(x=50, y=50)
# check switcher value
if videoloop_stop[0]:
# if switcher tells to stop then we switch it again and stop videoloop
videoloop_stop[0] = False
panel.destroy()
break
# videoloop_stop is a simple switcher between ON and OFF modes
videoloop_stop = [False]
root = tk.Tk()
root.geometry("1920x1080+0+0")
button1 = tk.Button(
root, text="start", bg="#fff", font=("", 50),
command=lambda: button1_clicked(videoloop_stop))
button1.place(x=1000, y=100, width=400, height=250)
button2 = tk.Button(
root, text="stop", bg="#fff", font=("", 50),
command=lambda: button2_clicked(videoloop_stop))
button2.place(x=1000, y=360, width=400, height=250)
root.mainloop()
I cannot fully test the code. Although the skeleton of the code (starting thread and stopping it by switching) works.
I have no experience with tkinter
. So I don't know what panel
is and can't tell if this approach is viable. Probably it is better to create panel
in the main thread of the code and pass the newly created panel
to the function button1_clicked()
and then to the function videoLoop()
. This will allow to control panel
directly from main thread but videoLoop()
should be modified significantly (including checking/exception_handling in the case when panel
is detroyed by the main thread).
Upvotes: 1