Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python: tkinter to display video from webcam and do a QR scan

I have been trying to create a tkinter top level window that streams video form webcam and do a QR scan. I got this QR scan code from SO and another code that just updates images from webcam instead of streaming the video on a tkinter label.

and i tried to combine these both so that a toplevel window with a label updating image from webcam and a close button to close the toplevel window. And while it streams the images, it can scan for QR code and if a scan is successful, the webcam and the toplevel window gets closed.

here is what i tried.

import cv2
import cv2.cv as cv
import numpy
import zbar
import time
import threading
import Tkinter
from PIL import Image, ImageTk

class BarCodeScanner(threading.Thread, Tkinter.Toplevel):
    def __init__(self):
        # i made this as a global variable so i can access this image
        # outside ie,. beyond the thread to update the image on to the  tkinter window
        global imgtk
        imgtk = None
        threading.Thread.__init__(self)
        self.WINDOW_NAME = 'Camera'
        self.CV_SYSTEM_CACHE_CNT = 5 # Cv has 5-frame cache
        self.LOOP_INTERVAL_TIME = 0.2 
        cv.NamedWindow(self.WINDOW_NAME, cv.CV_WINDOW_NORMAL)
        self.cam = cv2.VideoCapture(-1)
        self.confirm = 0

    def scan(self, aframe):
        imgray = cv2.cvtColor(aframe, cv2.COLOR_BGR2GRAY)
        # to show coloured image, as from the other code mentioned in the other code
        imgcol = cv2.cvtColor(aframe, cv2.COLOR_BGR2RGBA)
        imgcol_array = Image.fromarray(imgcol)
        imgtk = ImageTk.PhotoImage(image=imgcol_array)

        raw = str(imgray.data)
        scanner = zbar.ImageScanner()
        scanner.parse_config('enable')
        width = int(self.cam.get(cv.CV_CAP_PROP_FRAME_WIDTH))
        height = int(self.cam.get(cv.CV_CAP_PROP_FRAME_HEIGHT))
        imageZbar = zbar.Image(width, height,'Y800', raw)
        scanner.scan(imageZbar)

        for symbol in imageZbar:
            print 'decoded', symbol.type, 'symbol', '"%s"' % symbol.data
            return symbol.data

    def run(self):
        self.datalst = []
        print 'BarCodeScanner run', time.time()
        while True:                
            for i in range(0,self.CV_SYSTEM_CACHE_CNT):
                self.cam.read()
            img = self.cam.read()
            self.data = self.scan(img[1])

            cv2.imshow(self.WINDOW_NAME, img[1])
            cv.WaitKey(1)
            time.sleep(self.LOOP_INTERVAL_TIME)
            if self.data:
                self.datalst.append(self.data)
            # i have added this section so that it waits for scan
            # if a scan is made it and if gets same value after 2 scans
            # it has to stop webcam
            if len(self.datalst) == 2 and len(set(self.datalst)) <= 1:
                # I want to close the webcam before closing the toplevel window
                #self.cam.release()
                #cv2.destroyAllWindows()
                break
        self.cam.release()

def Video_Window():
    video_window = Tkinter.Toplevel()
    video_window.title('QR Scan !!')
    img_label = Tkinter.Label(video_window)
    img_label.pack(side=Tkinter.TOP)
    close_button = Tkinter.Button(video_window, text='close', command = video_window.destroy)
    close_button.pack(side=Tkinter.TOP)

    def update_frame():
        global imgtk
        img_label.configure(image=imgtk)
        img_label.after(10,update_frame)
    update_frame()

def main():
    root = Tkinter.Tk()
    button_scanQr = Tkinter.Button(root, text='QR Scan', command=start_scan)
    button_scanQr.pack()
    root.mainloop()

def start_scan():
    scanner = BarCodeScanner()
    scanner.start()

    Video_Window()
    #scanner.join()

main()

Problem is,

  1. I actually wanted to display the video on the Toplevel window, not the OpenCV window
  2. at the same time do a QR Scan,if a read is sucessfull, the Toplevel window should close without abruptly closing webcam(because, when i try to use self.cam.release() or cv2.destroyAllWindows() my webcams lights or on even if i forcefully terminate the programs compilation).

Now what i get is a separate window created by OpenCV that streams video inside. But i don’t want that window, instead i want the video to be displayed on the tkinter's toplevel window. also when there is a sucessfull read, the webcam stucks at the final image it reads.

i tried to remove the line that was responsible for OpenCV window, inside the run method of BarcodeScanner class

cv2.imshow(self.WINDOW_NAME, img[1])

it still showed up with a different window with no output, and if i try to close that window, it created another one similar and recursively.

UPDATE:

As i noticed i made some silly mistakes without understanding of some lines in cv2, i made some change on the code by adding the toplevel window code into the run method of the class(im not sure if this is a right way).

import cv2
import cv2.cv as cv
import numpy
import zbar
import time
import threading
import Tkinter
from multiprocessing import Process, Queue
from Queue import Empty
from PIL import Image, ImageTk

class BarCodeScanner(threading.Thread, Tkinter.Toplevel):
    def __init__(self):
        threading.Thread.__init__(self)
        #self.WINDOW_NAME = 'Camera'
        self.CV_SYSTEM_CACHE_CNT = 5 # Cv has 5-frame cache
        self.LOOP_INTERVAL_TIME = 0.2
        #cv.NamedWindow(self.WINDOW_NAME, cv.CV_WINDOW_NORMAL)
        self.cam = cv2.VideoCapture(-1)
        # check if webcam device is free
        self.proceede = self.cam.isOpened()
        if not self.proceede:
            return
        self.confirm = 0

    def scan(self, aframe):
        imgray = cv2.cvtColor(aframe, cv2.COLOR_BGR2GRAY)
        raw = str(imgray.data)
        scanner = zbar.ImageScanner()
        scanner.parse_config('enable')          
        width = int(self.cam.get(cv.CV_CAP_PROP_FRAME_WIDTH))
        height = int(self.cam.get(cv.CV_CAP_PROP_FRAME_HEIGHT))
        imageZbar = zbar.Image(width, height,'Y800', raw)
        scanner.scan(imageZbar)
        for symbol in imageZbar:
            print 'decoded', symbol.type, 'symbol', '"%s"' % symbol.data
            return symbol.data

    def run(self):
        if not self.proceede:
            return
        def show_frame():
            _, img = self.cam.read()
            img = cv2.flip(img,1)
            cv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)
            img = Image.fromarray(cv2image)
            imgtk = ImageTk.PhotoImage(image=img)
            img_label.imgtk = imgtk
            img_label.configure(image=imgtk)
            video_window.after(250, show_frame)

        def destroy_video_window():
            self.cam.release()
            video_window.destroy()

        # Toplevel GUI
        video_window = Tkinter.Toplevel()
        video_window.title('QR Scan !!')
        img_label = Tkinter.Label(video_window)
        img_label.pack(side=Tkinter.TOP)
        close_button = Tkinter.Button(video_window, text='close', command = destroy_video_window)
        close_button.pack(side=Tkinter.RIGHT)
        show_frame()

        self.datalst = []
        print 'BarCodeScanner run', time.time()
        while True:
            for i in range(0,self.CV_SYSTEM_CACHE_CNT):
                self.cam.read()
            img = self.cam.read()
            self.data = self.scan(img[1])
            time.sleep(self.LOOP_INTERVAL_TIME)
            if self.data:
                self.datalst.append(self.data)
            if len(self.datalst) == 2 and len(set(self.datalst)) <= 1:
                video_window.destroy()
                break
        self.cam.release()

def main():
    root = Tkinter.Tk()
    button_scanQr = Tkinter.Button(root, text='QR Scan', command=scaner)
    button_scanQr.pack()
    root.mainloop()

def scaner():
    scanner = BarCodeScanner()
    scanner.start()

main()

now, I can get the image on the Toplevel window, But i dont know how to close the webcam.

condition 1: when i show a QR code to scan, it reads it successfully and webcam quits without any error.

condition 2: when i click the close button on the toplevel window(say if user doesn't want to do any scan and just want to close the webcam) i get error saying

libv4l2: error dequeuing buf: Invalid argument
VIDIOC_DQBUF: Invalid argument
select: Bad file descriptor
VIDIOC_DQBUF: Bad file descriptor
select: Bad file descriptor
VIDIOC_DQBUF: Bad file descriptor
Segmentation fault (core dumped)

I am writing this application for Linux, Mac and Windows machine. How can i close or terminate the webcam safely.

like image 265
arvindh Avatar asked May 07 '16 13:05

arvindh


1 Answers

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 cv2
import cv2.cv as cv
import zbar
import time
import threading
import Tkinter as tk

from PIL import Image, ImageTk

class 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()
like image 151
J.J. Hakala Avatar answered Oct 14 '22 19:10

J.J. Hakala