python: tkinter to display video from webcam and do a QR scan
Your program has two threads, the main thread and the worker thread that reads frames from the camera. When the close button is clicked, it happens in the main thread. After self.cam.release()
the object self.cam
is probably in an unusable state, and when a method of self.cam
is called by the worker thread, there may be some trouble. Maybe the implementation of cv2.VideoCapture
is faulty and it should throw some exception when that happens.
Accessing tkinter widgets from other thread than the main thread may also cause problems.
For clean program termination, creating an instance of threading.Event
and then checking for event.is_set()
at some point in the work thread could work. For example
def destroy_video_window(): self.stop_event.set() video_window.destroy()
and then in the worker thread
while True: if self.stop_event.is_set(): break for i in range(0, self.CV_SYSTEM_CACHE_CNT): self.cam.read()
There are several things that could be done in other way, the following is a modified version of the code. It avoids calling tkinter methods from other thread than the main thread, event_generate()
being the only tkinter method called by the worker thread. Explicit polling is avoided by emitting virtual events, for example <<ScannerQuit>>
, that are placed in the tkinter event queue.
import cv2import cv2.cv as cvimport zbarimport timeimport threadingimport Tkinter as tkfrom PIL import Image, ImageTkclass Scanner(object): def __init__(self, handler, *args, **kw): self.thread = threading.Thread(target=self.run) self.handler = handler self.CV_SYSTEM_CACHE_CNT = 5 # Cv has 5-frame cache self.LOOP_INTERVAL_TIME = 0.2 self.cam = cv2.VideoCapture(-1) self.scanner = zbar.ImageScanner() self.scanner.parse_config('enable') self.cam_width = int(self.cam.get(cv.CV_CAP_PROP_FRAME_WIDTH)) self.cam_height = int(self.cam.get(cv.CV_CAP_PROP_FRAME_HEIGHT)) self.last_symbol = None def start(self): self.thread.start() def scan(self, aframe): imgray = cv2.cvtColor(aframe, cv2.COLOR_BGR2GRAY) raw = str(imgray.data) image_zbar = zbar.Image(self.cam_width, self.cam_height, 'Y800', raw) self.scanner.scan(image_zbar) for symbol in image_zbar: return symbol.data def run(self): print 'starting scanner' while True: if self.handler.need_stop(): break # explanation for this in # http://stackoverflow.com/a/35283646/5781248 for i in range(0, self.CV_SYSTEM_CACHE_CNT): self.cam.read() img = self.cam.read() self.handler.send_frame(img) self.data = self.scan(img[1]) if self.handler.need_stop(): break if self.data is not None and (self.last_symbol is None or self.last_symbol <> self.data): # print 'decoded', symbol.type, 'symbol', '"%s"' % symbol.data self.handler.send_symbol(self.data) self.last_symbol = self.data time.sleep(self.LOOP_INTERVAL_TIME) self.cam.release()class ScanWindow(tk.Toplevel): def __init__(self, parent, gui, *args, **kw): tk.Toplevel.__init__(self, master=parent, *args, **kw) self.parent = parent self.gui = gui self.scanner = None self.lock = threading.Lock() self.stop_event = threading.Event() self.img_label = tk.Label(self) self.img_label.pack(side=tk.TOP) self.close_button = tk.Button(self, text='close', command=self._stop) self.close_button.pack() self.bind('<Escape>', self._stop) parent.bind('<<ScannerFrame>>', self.on_frame) parent.bind('<<ScannerEnd>>', self.quit) parent.bind('<<ScannerSymbol>>', self.on_symbol) def start(self): self.frames = [] self.symbols = [] class Handler(object): def need_stop(self_): return self.stop_event.is_set() def send_frame(self_, frame): self.lock.acquire(True) self.frames.append(frame) self.lock.release() self.parent.event_generate('<<ScannerFrame>>', when='tail') def send_symbol(self_, data): self.lock.acquire(True) self.symbols.append(data) self.lock.release() self.parent.event_generate('<<ScannerSymbol>>', when='tail') self.stop_event.clear() self.scanner = Scanner(Handler()) self.scanner.start() self.deiconify() def _stop(self, *args): self.gui.stop() def stop(self): if self.scanner is None: return self.stop_event.set() self.frames = [] self.symbols = [] self.scanner = None self.iconify() def quit(self, *args): self.parent.event_generate('<<ScannerQuit>>', when='tail') def on_symbol(self, *args): self.lock.acquire(True) symbol_data = self.symbols.pop(0) self.lock.release() print 'symbol', '"%s"' % symbol_data self.after(500, self.quit) def on_frame(self, *args): self.lock.acquire(True) frame = self.frames.pop(0) self.lock.release() _, img = frame img = cv2.flip(img, 1) cv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) imgtk = ImageTk.PhotoImage(image=img) self.img_label.imgtk = imgtk self.img_label.configure(image=imgtk)class GUI(object): def __init__(self, root): self.root = root self.scan_window = ScanWindow(self.root, self) self.scan_window.iconify() self.root.title('QR Scan !!') self.lframe = tk.Frame(self.root) self.lframe.pack(side=tk.TOP) self.start_button = tk.Button(self.lframe, text='start', command=self.start) self.start_button.pack(side=tk.LEFT) self.stop_button = tk.Button(self.lframe, text='stop', command=self.stop) self.stop_button.configure(state='disabled') self.stop_button.pack(side=tk.LEFT) self.close_button = tk.Button(self.root, text='close', command=self.quit) self.close_button.pack(side=tk.TOP) self.root.bind('<<ScannerQuit>>', self.stop) self.root.bind('<Control-s>', self.start) self.root.bind('<Control-q>', self.quit) self.root.protocol('WM_DELETE_WINDOW', self.quit) def start(self, *args): self.start_button.configure(state='disabled') self.scan_window.start() self.stop_button.configure(state='active') def stop(self, *args): self.scan_window.stop() self.start_button.configure(state='active') self.stop_button.configure(state='disabled') def quit(self, *args): self.scan_window.stop() self.root.destroy()def main(): root = tk.Tk() gui = GUI(root) root.mainloop()main()