python: tkinter显示来自网络摄像头的视频并进行QR扫描

时间:2023-02-01 20:40:36

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.

我一直在尝试创建一个tkinter*窗口,可以从网络摄像头中播放视频并进行QR扫描。我从SO那里得到了这个二维码和另一个代码,它只是更新网络摄像头的图像,而不是在tkinter标签上播放视频。

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.

我试着把这两者结合起来这样一个顶层窗口和一个标签更新图片从webcam和一个关闭按钮关闭顶层窗口。当它传输图像时,它可以扫描二维码,如果扫描成功,摄像头和顶层窗口就会关闭。

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. 实际上,我想在顶层窗口中显示视频,而不是OpenCV窗口
  3. 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).
  4. 同时进行二维码扫描,如果读取成功,顶部窗口应该在没有突然关闭网络摄像头的情况下关闭(因为,当我尝试使用self.cam.release()或cv2.destroyAllWindows()时,我的摄像头会亮起,即使我强行终止程序编译)。

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.

现在我得到的是一个由OpenCV创建的独立窗口,在里面播放视频。但我不想要那个窗口,我想要视频显示在tkinter的顶层窗口。当有一个成功的阅读时,网络摄像头会在最后的图片上看到它。

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

我试图删除在BarcodeScanner类的run方法中负责OpenCV窗口的行

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).

正如我所注意到的,我在不理解cv2中的某些代码的情况下犯了一些愚蠢的错误,我对代码做了一些修改,在类的run方法中添加了*窗口代码(我不确定这是否正确)。

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.

条件1:当我显示二维码扫描时,它成功读取了二维码,摄像头没有出现任何错误就退出了。

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

条件2:当我点击顶部窗口上的关闭按钮(如果用户不想进行任何扫描,只想关闭网络摄像头)时,我得到错误消息

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.

我正在为Linux、Mac和Windows机器编写这个应用程序。如何安全关闭或终止网络摄像头?

1 个解决方案

#1


1  

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.

你的程序有两个线程,主线程和从摄像机中读取帧的工作线程。当关闭按钮被单击时,它发生在主线程中。在self。cam.release()对象self之后。cam可能处于不可用的状态,以及自我方法的时候。凸轮被工作线程调用,可能会有一些麻烦。也许是cv2的实现。VideoCapture是有缺陷的,当它发生时它应该抛出一些异常。

Accessing tkinter widgets from other thread than the main thread may also cause problems.

从主线程以外的其他线程访问tkinter小部件也可能导致问题。

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

对于干净的程序终止,创建线程实例。事件,然后在工作线程的某个点检查Event .is_set()。例如

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.

还有一些事情可以用其他的方式完成,下面是代码的修改版本。它避免从主线程以外的其他线程调用tkinter方法,event_generate()是工作线程调用的惟一tkinter方法。通过发出虚拟事件(例如< >)来避免显式轮询,这些事件被放置在tkinter事件队列中。

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://*.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()

#1


1  

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.

你的程序有两个线程,主线程和从摄像机中读取帧的工作线程。当关闭按钮被单击时,它发生在主线程中。在self。cam.release()对象self之后。cam可能处于不可用的状态,以及自我方法的时候。凸轮被工作线程调用,可能会有一些麻烦。也许是cv2的实现。VideoCapture是有缺陷的,当它发生时它应该抛出一些异常。

Accessing tkinter widgets from other thread than the main thread may also cause problems.

从主线程以外的其他线程访问tkinter小部件也可能导致问题。

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

对于干净的程序终止,创建线程实例。事件,然后在工作线程的某个点检查Event .is_set()。例如

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.

还有一些事情可以用其他的方式完成,下面是代码的修改版本。它避免从主线程以外的其他线程调用tkinter方法,event_generate()是工作线程调用的惟一tkinter方法。通过发出虚拟事件(例如< >)来避免显式轮询,这些事件被放置在tkinter事件队列中。

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://*.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()