본문 바로가기
PyQt5

PyQt5를 이용하여 동영상 플레이어를 만들어보자(4)

by Tripleler 2022. 1. 10.

이전 포스팅

https://tripleler.tistory.com/6

 

PyQt5를 이용하여 동영상 플레이어를 만들어보자(3)

이전포스팅 https://tripleler.tistory.com/5?category=1015667 PyQt5를 이용하여 동영상 플레이어를 만들어보자(2) 프로그래밍에서 중요한 것은 쓰레딩이다. 쓰레딩을 간단히 설명하자면 일반적으로 파이썬 코

tripleler.tistory.com

 

이제 마지막으로 동영상프레임 슬라이더 하나만 더 구현하면 어엿한 동영상 프로그램이 된다.

마지막인 만큼 코드도 많이 복잡하다.

 

우선 코드부터 공개한다.

이전포스팅과 마찬가지로 코드복사는 금지이다.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtTest import QTest
import cv2
import numpy as np

app = QApplication(sys.argv)
screen = app.primaryScreen()
size = screen.size()
width = size.width()
height = size.height()


class MyApp(QMainWindow):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.cent_widget = CentWidget()
        self.setCentralWidget(self.cent_widget)
        self.setWindowTitle('Video Player')
        self.setGeometry(width // 10, height // 10, width // 5 * 4, height // 5 * 4)
        menubar = self.menuBar()
        file = QMenu('파일', self)
        menubar.addMenu(file)
        load = QAction('불러오기', self)
        file.addAction(load)
        load.triggered.connect(self.cent_widget.load)
        self.show()


class CentWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.videothread = VideoThread()
        base = QPixmap('base.JPG')

        self.lbl_img = QLabel()
        self.lbl_img.setScaledContents(True)
        self.lbl_img.setPixmap(base)

        icon_pp = QIcon()
        icon_pp.addPixmap(QPixmap("./icon/pause.png"), QIcon.Normal, QIcon.On)
        icon_pp.addPixmap(QPixmap("./icon/play.png"), QIcon.Active, QIcon.Off)
        icon_pp.addPixmap(QPixmap("./icon/pause.png"), QIcon.Active, QIcon.On)

        self.btn_pp = QPushButton()
        self.btn_pp.setCheckable(True)
        self.btn_pp.setIcon(icon_pp)
        self.btn_pp.clicked.connect(self.pp)

        speed_opt = QComboBox(self)
        [speed_opt.addItem(i) for i in ['빠르게', '보통', '느리게']]
        speed_opt.setCurrentIndex(1)
        speed_opt.activated.connect(self.videothread.speed)

        self.sliframe = QSlider(Qt.Horizontal, self)
        self.sliframe.setSingleStep(1)
        self.sliframe.setRange(0, 0)
        self.sliframe.valueChanged.connect(self.frame_chg)
        self.sliframe.sliderPressed.connect(self.videothread.pause)
        self.sliframe.sliderReleased.connect(self.videothread.play)

        grid = QGridLayout()
        grid.addWidget(self.lbl_img, 0, 0, 19, 20)
        grid.addWidget(self.btn_pp, 19, 0, 1, 5)
        grid.addWidget(speed_opt, 19, 5, 1, 5)
        grid.addWidget(self.sliframe, 19, 10, 1, 10)
        self.setLayout(grid)

        self.videothread.send_img.connect(self.show)
        self.videothread.send_frames.connect(self.set_frames)
        self.videothread.send_frame.connect(self.sli_chg)
        self.videothread.send_status.connect(self.status)

    def show(self, frame):
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        img = QImage(frame.data, frame.shape[1], frame.shape[0], frame.shape[2] * frame.shape[1],
                     QImage.Format_RGB888)
        self.lbl_img.setPixmap(QPixmap.fromImage(img))

    def load(self):
        fname = QFileDialog.getOpenFileName(self, '동영상 선택하기', filter='*.mp4')
        if fname[0]:
            if self.videothread.isRunning():
                self.videothread.status = False
                QTest.qWait(1000)
                self.videothread.cap.release()
                self.videothread.status = True
                self.videothread.terminate()
            self.videothread.source = fname[0]
            self.btn_pp.setChecked(True)
            self.videothread.start()

    def pp(self):
        if self.btn_pp.isChecked():
            self.videothread.play()
        else:
            self.videothread.pause()

    def frame_chg(self):
        num = int(self.sliframe.value())
        if not self.videothread.status:
            self.videothread.cap.set(1, num)
            self.videothread.frame = num

    def set_frames(self, frames):
        self.sliframe.setRange(1, frames)

    def sli_chg(self, frame):
        self.sliframe.setValue(frame)

    def status(self, sign):
        if sign and not self.btn_pp.isChecked():
            self.btn_pp.setChecked(True)
        if not sign and self.btn_pp.isChecked():
            self.btn_pp.setChecked(False)


class VideoThread(QThread):
    send_img = pyqtSignal(np.ndarray)
    send_frame = pyqtSignal(int)
    send_frames = pyqtSignal(int)
    send_status = pyqtSignal(bool)

    def __init__(self):
        super().__init__()
        self.source = ''
        self.cond = QWaitCondition()
        self.status = True
        self.mutex = QMutex()
        self.delay = 30

    def run(self):
        self.cap = cv2.VideoCapture(self.source)
        if not self.cap.isOpened():
            print("Camera open failed!")
        self.send_frames.emit(int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)))
        self.frame = 0
        while True:
            self.mutex.lock()
            if not self.status:
                self.cond.wait(self.mutex)
            ret, frame = self.cap.read()
            if ret:
                self.frame += 1
                self.send_frame.emit(self.frame)
            else:
                self.mutex.unlock()
                self.status = False
                continue
            self.send_img.emit(frame)
            cv2.waitKey(self.delay)
            self.mutex.unlock()

    def play(self):
        self.status = True
        self.send_status.emit(True)
        self.cond.wakeAll()

    def pause(self):
        self.status = False
        self.send_status.emit(False)

    def speed(self, e):
        if not e:
            self.delay = 1
        elif e == 1:
            self.delay = 30
        elif e == 2:
            self.delay = 100


ex = MyApp()
sys.exit(app.exec_())

우선 CentWidget클래스에 sliframe이라는 슬라이더 바를 구현했다.

self.sliframe = QSlider(Qt.Horizontal, self)
self.sliframe.setSingleStep(1)
self.sliframe.setRange(0, 0)
self.sliframe.valueChanged.connect(self.frame_chg)
self.sliframe.sliderPressed.connect(self.videothread.pause)
self.sliframe.sliderReleased.connect(self.videothread.play)

Qt.Horizontal 옵션을 주면 가로 슬라이더, Qt.Vertical 옵션을 주면 세로 슬라이더를 만들 수 있다.

슬라이더의 값이 변하면 frame_chg라는 함수를 호출하고, 

슬라이더를 누르면 쓰레드가 멈추도록,

슬라이더를 놓아주면 쓰레드가 실행되도록 설계했다.

def frame_chg(self):
    num = int(self.sliframe.value())
    if not self.videothread.status:
        self.videothread.cap.set(1, num)
        self.videothread.frame = num

쓰레드가 실행중이면 에러를 일으킬 수 있기 때문에

쓰레드가 일시중지된 상태에서만 cap.set 함수를 이용하여 프레임을 맞추도록 했다.

send_frame = pyqtSignal(int)
send_frames = pyqtSignal(int)
send_status = pyqtSignal(bool)

쓰레드에서는 다음 3가지 시그널을 추가했다.

파일을 불러론 뒤 총 프레임을 CentWidget 클래스로 쏘아주고

매 프레임마다 현재 프레임을 CentWidget 클래스로 쏘아준다.

또한 슬라이더를 누르고 놓을 때마다 재생/정지 버튼도 변환되도록 불리언 시그널도 쏘게 했다.

 

이제 좀 어엿한 동영상 플레이어 같다!!

 

총 179줄의 코드만을 사용하여 아주 간단한 동영상 프로그램을 만들어보았다.

물론 UI가 좀... 심각하게 구리지만 이것도 꾸미면 되는거고

애초에 이건 내가 진행하는 나만의 '개인 프로젝트'이다.

 

 

 

내가 처음 pyqt5를 배우게 된 계기는

YOLOv5와 연계된 영상인식 동영상 플레이어를 만드는 프로젝트를 한 공공기관과 진행하게 되었다.

필자는 한달 뒤인 2월에 졸업예정인 4학년이고, 통계학과생인 프로그래밍 비전공자다. (R만 씀)

 

파이썬이라는 언어를 교양으로 배운게 전부라서 리스트(list)자료형과 판다스의 데이터프레임, matplotlib의 그래프를 그리는 수준밖에 안되었는데 어쩌다 보니..;;; 진짜 사람 인생은 언제 어떻게 될지 도저히 모르겠다.

 

아무튼 YOLOv5와 연계된 플레이어를 11월부터 만들기로 하였는데 아는 언어라고는 파이썬이 전부라서 pyqt5를 사용하기로 깨닫고 사용법에 익숙해지는데만 한 달을 잡아먹었다.

 

다행이 1월 6일 프로젝트를 잘 마무리하고 이제라도 블로그를 만들어서 정리해야겠다 싶은데

비밀유지계약서가 어디까지 적용되는지 모르겠어서 일단 나만의 미니 프로젝트를 진행해보았다.

 

위키독스, stackoverflow 등 진짜 많은 사이트를 구글링하고 팀원들과 소통하면서 하나씩 배우면서 가장 힘들던게 클래스와 쓰레드 구현이었다. 이 포스팅을 통해서 많은 사람들이 pyqt5에 좀 더 쉽게 적응하기를 바란다.

 

YOLOv5에 대한 포스팅도 곧 할텐데 카테고리를 따로 두어 작성할 예정이다.

댓글