Создать графический интерфейс, который может включать / выключать изображения с камеры, используя Python 3 и tkinter - PullRequest
0 голосов
/ 01 октября 2018

Что я хочу сделать

  1. Создать главное окно с двумя кнопками «кнопка запуска» и «кнопка остановки»

  2. Когда нажата «кнопка пуска», изображения подключенной USB-камеры отображаются в главном окне

  3. Нажмите «Стоп», чтобы стереть изображение с USB-камерыотображается в [2] (выход из главного окна)

Неисправность

[1] и [2] завершена.Тем не менее, невозможно стереть изображение с USB-камеры с помощью [3].Сообщение об ошибке:

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' 

Код

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

Ответы [ 2 ]

0 голосов
/ 01 октября 2018

Есть некоторые фундаментальные проблемы с тем, что вы делаете здесь.Основная предпосылка неверна в том, что вы не должны постоянно уничтожать и заново создавать виджет Label для отображения изображения.Вместо этого просто обновите изображение, которое прикреплено к существующему виджету, вызвав его метод configure () с новым изображением.Это исправление производительности независимо от проблем с потоками, которые у вас здесь есть.В общем, создайте виджеты один раз и обновите их.Это позволяет избежать каскада событий изменения геометрии, возникающих при удалении и добавлении виджетов из дерева пользовательского интерфейса.

Потоковый дизайн здесь некорректен.Вы не должны делать Tk звонки из рабочих потоков.Tk привязан к одному потоку, и между потоками должны проходить только события.Чтобы показать, как это может быть лучше сконструировано, я изменил код, чтобы использовать queue.Queue() для передачи фрейма изображения из потока чтения opencv в поток Tk.Мы можем опубликовать пользовательское событие, чтобы уведомить пользовательский интерфейс о готовности нового кадра (<<MessageGenerated>>).

Последнее замечание: вы должны хранить ссылку на изображение, которое добавляете в ярлык Tk, иначеможет собрать мусор, когда вы этого не ожидаете.Поэтому мы обновляем элемент self.photo с каждым новым изображением.

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

Я должен добавить, что на этом этапе, если вы хотите показать пустое изображение после нажатия кнопки «Стоп», вы можете установить метку просмотра.виджет для нового пустого изображения, как показано в конструкторе.

0 голосов
/ 01 октября 2018

Ошибка, которую вы получили, фактически говорит о том, что именно не так с кодом.TypeError: destroy() missing 1 required positional argument: 'panel' буквально говорит, что вы должны передать аргумент panel в функцию destroy().Вы вызываете функцию неявно с помощью thread.start() в наборе button2_clicked().Чтобы решить эту проблему, вы должны изменить создание объекта потока:

thread = threading.Thread(target=destroy, args=(panel,))

Также вы должны передать panel в button2_clicked() функцию.Здесь возникает другая проблема, поскольку функция videoloop() возвращает panel.Так что panel никогда не возвращается, потому что videoloop() содержит бесконечный цикл while.Чтобы решить эту проблему, вам нужен способ передачи данных между операционными кодами вашего кода.Например, вы можете сделать это следующим образом (простой, но не надежный подход):

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

Я не могу полностью протестировать код.Хотя скелет кода (запуск потока и остановка его переключением) работает.

У меня нет опыта работы с tkinter.Поэтому я не знаю, что такое panel, и не могу сказать, жизнеспособен ли этот подход.Возможно, лучше создать panel в главном потоке кода и передать вновь созданный panel в функцию button1_clicked(), а затем в функцию videoLoop().Это позволит контролировать panel непосредственно из основного потока, но videoLoop() следует значительно изменить (включая проверку / обработку исключений в случае, когда основной поток уничтожает panel).

...