본문 바로가기
파이썬 프로그래밍/PyQt5 공부하기

7. PyQt5 쓰레드 사용하기

by Majestyblue 2024. 11. 25.

6. PyQt5와 opencv 연동하기 : 동영상 불러오기

 

6. PyQt5와 opencv 연동하기 : 동영상 불러오기

이번도 악보쓰는 프로그래머 블로그를 참고하였다.OpenCV(Python) + PyQt OpenCV(Python) + PyQtOpenCV로 영상처리나 컴퓨터 비전을 처리하고 나서 결과를 화면에 표시하려면 결국 창을 띄워야 하는데, OpenCV

toyourlight.tistory.com

저번 포스트에서 PyQt5 이벤트 loop에 함부로 반복문을 사용하면 안된다고 하였다. 반복문에 진입하게 되면 이벤트 loop이 막히기 때문이다. 아래는 그러한 문제를 발생시키는 예시이다.

 

카운트 시작이라는 버튼을 누르면 0.5초가 지날 때 마다 1씩 감소하는 코드다. 여기서 '카운트 초기화'라는 버튼을 눌러도 카운트가 초기화되지 않는다. 0.5초마다 -1씩 연산하는 while문 때문에 이벤트 loop이 멈추었기 때문이다. 

 

import sys 
from PyQt5.QtWidgets import *
from PyQt5.QtGui import * 
import time

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.initUI()
        
    def initUI(self):
        self.setWindowTitle("physics Lab")
        self.setGeometry(300, 300, 400, 400) 
        self.setWindowIcon(QIcon("physics.png"))
        
        self.cnt_label = QLabel(parent=self)
        self.cnt_label.setText("10부터 카운트 시작")
        
        btn_count = QPushButton(text="카운트 시작", parent=self)
        btn_count.clicked.connect(self.count)
        
        btn_clear = QPushButton(text="카운트 초기화", parent=self)
        btn_clear.clicked.connect(self.clear)
        
        vbox = QVBoxLayout()
        hbox = QHBoxLayout()
        
        vbox.addStretch(1)
        vbox.addWidget(self.cnt_label)
        vbox.addWidget(btn_count)
        vbox.addWidget(btn_clear)
        vbox.addStretch(1)
        
        hbox.addStretch(1)
        hbox.addLayout(vbox)
        hbox.addStretch(1)
        
        widget = QWidget()
        widget.setLayout(hbox)
        self.setCentralWidget(widget)
        
    def count(self):
        count_value = 10
        self.cnt_label.setText(str(count_value))
        
        while count_value > 0:
            time.sleep(0.5)  # 0.5초 대기
            count_value -= 1
            self.cnt_label.setText(str(count_value))
            QApplication.processEvents()  # GUI 업데이트를 위해 이벤트 처리
            
    def clear(self):
        self.cnt_label.clear()
        self.cnt_label.setText("10부터 카운트 시작")
        QApplication.processEvents()
        pass
        

app = QApplication(sys.argv)
win = MyWindow()
win.show()
app.exec_()

 

이를 해결하기 위해 count 연산을 이벤트 loop에 영향 받지 않도록 다른 곳에서 돌려야 한다. 이 때 필요한 것이 바로 Thread이다. 아래 코드를 확인해 보자.

 

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import time
from queue import Queue
from threading import Thread

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.initUI()
        
        self.worker_thread = None
        self.queue = Queue()
        self.is_running = False  # 카운트 상태를 추적

    def initUI(self):
        self.setWindowTitle("Physics Lab")
        self.setGeometry(300, 300, 400, 400) 
        self.setWindowIcon(QIcon("physics.png"))
        
        self.cnt_label = QLabel("10부터 카운트 시작", parent=self)
        
        btn_count = QPushButton(text="카운트 시작", parent=self)
        btn_count.clicked.connect(self.start_count)
        
        btn_clear = QPushButton(text="카운트 초기화", parent=self)
        btn_clear.clicked.connect(self.reset_count)
        
        vbox = QVBoxLayout()
        hbox = QHBoxLayout()
        
        vbox.addStretch(1)
        vbox.addWidget(self.cnt_label)
        vbox.addWidget(btn_count)
        vbox.addWidget(btn_clear)
        vbox.addStretch(1)
        
        hbox.addStretch(1)
        hbox.addLayout(vbox)
        hbox.addStretch(1)
        
        widget = QWidget()
        widget.setLayout(hbox)
        self.setCentralWidget(widget)

    def start_count(self):
        if self.worker_thread is None or not self.worker_thread.is_alive():
            self.is_running = True  # 카운트 시작 상태로 설정
            self.cnt_label.setText("카운트 시작")
            self.queue = Queue()
            self.worker_thread = Thread(target=self.count, daemon=True)
            self.worker_thread.start()
            self.update_gui()

    def count(self):
        count_value = 10
        while count_value >= 0 and self.is_running:  # 카운트 상태에 따라 반복
            self.queue.put(count_value)
            self.update_gui()
            time.sleep(0.5)
            count_value -= 1

    def reset_count(self):
        self.is_running = False  # 카운트 중지
        self.cnt_label.setText("카운트 초기화됨")  # 라벨 업데이트
        QApplication.processEvents()
        if self.worker_thread is not None and self.worker_thread.is_alive():
            self.worker_thread.join()  # 스레드가 종료될 때까지 대기

    def update_gui(self):
        # Queue에서 데이터가 있고 Thread가 실행중이라면 라벨 업데이트
        if not self.queue.empty() and self.is_running:
            count_value = self.queue.get()
            self.cnt_label.setText(str(count_value))
            QApplication.processEvents()
        

app = QApplication(sys.argv)
win = MyWindow()
win.show()
app.exec_()

 

이 코드는 count라는 작업을 이벤트 loop이 작동하는 메인 스레드가 아닌 다른 스레드에서 작업한다. 그래서 이벤트 loop이 원활하게 돌아갈 수 있다. 각 함수의 역할을 하나씩 알아보자.

 

1. start_count 함수

    def start_count(self):
        if self.worker_thread is None or not self.worker_thread.is_alive():
            self.is_running = True  # 카운트 시작 상태로 설정
            self.cnt_label.setText("카운트 시작")
            self.queue = Queue()
            self.worker_thread = Thread(target=self.count, daemon=True)
            self.worker_thread.start()
            self.update_gui()
  • start_count 함수는 카운트를 시작하는 역할을 한다. 이 함수는 먼저 현재 실행 중인 스레드가 없거나 종료된 경우에만 실행되도록 조건을 설정한다. 
  • 카운트를 시작하는 상태로 is_running 변수를 True로 설정하고, 라벨의 텍스트를 "카운트 시작"으로 변경한다. 
  • 이후 새로운 큐를 생성하고, 카운트를 수행할 스레드를 생성하여 시작한다. 
  • 마지막으로 update_gui 함수를 호출하여 GUI를 업데이트한다.

 

2. count 함수

    def count(self):
        count_value = 10
        while count_value >= 0 and self.is_running:  # 카운트 상태에 따라 반복
            self.queue.put(count_value)
            self.update_gui()
            time.sleep(0.5)
            count_value -= 1
  • count 함수는 실제 카운트를 수행하는 메인 로직을 포함하고 있다. 이 함수는 초기값으로 10을 설정하고, is_running이 True인 동안 카운트를 계속 진행한다.
  •  매 반복마다 현재 카운트 값을 큐에 넣고, 0.5초의 지연을 두어 카운트를 감소시킨다. 카운트가 0 이하로 떨어지거나 is_running이 False로 변경될 때까지 반복된다.

 

 

3. reset_count 함수

    def reset_count(self):
        self.is_running = False  # 카운트 중지
        self.cnt_label.setText("카운트 초기화됨")  # 라벨 업데이트
        QApplication.processEvents()
        if self.worker_thread is not None and self.worker_thread.is_alive():
            self.worker_thread.join()  # 스레드가 종료될 때까지 대기
  • reset_count 함수는 카운트를 초기화하는 역할을 한다. 
  • 이 함수는 먼저 is_running 변수를 False로 설정하여 카운트를 중지하고, 라벨의 텍스트를 "카운트 초기화됨"으로 변경한다. 
  • 이후 QApplication.processEvents()를 호출하여 GUI가 즉시 업데이트되도록 한다. 
  • 만약 카운트 스레드가 여전히 실행 중이라면, join() 메서드를 호출하여 스레드가 종료될 때까지 기다린다.

 

 

4. update_gui 함수

    def update_gui(self):
        # Queue에서 데이터가 있고 Thread가 실행중이라면 라벨 업데이트
        if not self.queue.empty() and self.is_running:
            count_value = self.queue.get()
            self.cnt_label.setText(str(count_value))
            QApplication.processEvents()
  • update_gui 함수는 GUI를 업데이트하는 역할을 한다. 
  • 이 함수는 큐에 데이터가 있고, is_running이 True인 경우에만 실행된다. 
  • 큐에서 카운트 값을 꺼내와서 라벨의 텍스트를 해당 값으로 변경하고, QApplication.processEvents()를 호출하여 GUI가 즉시 반영되도록 한다.

 

실행결과를 확인해 보자.

결론을 이야기하자면, 카운트를 계산하는 count 함수는 별도의 스레드에서 실행되어 카운트 값을 계산한다. 그리고 메인 스레드는 카운트값을 Label에 표시한다. count_value라는 값을 각각의 스레드에서 통신해야 하므로 Queue를 사용하는 것이다. 

 

이 개념을 이용한다면 동영상을 불러와서 넘파이로 변환하는 것을 별도의 스레드에서 처리하고, 변환된 넘파이를 메인 스레드에서 화면에 출력할 수 있으며 넘파이 이미지를 Queue로 통신하면 동영상을 제어할 수 있음을 생각해 볼 수 있겠다.