이전 포스팅
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에 대한 포스팅도 곧 할텐데 카테고리를 따로 두어 작성할 예정이다.
'PyQt5' 카테고리의 다른 글
QSplitter 다루기 (0) | 2022.05.16 |
---|---|
사진에 바운딩박스 그리기 (0) | 2022.03.28 |
PyQt5를 이용하여 동영상 플레이어를 만들어보자(3) (0) | 2022.01.08 |
PyQt5를 이용하여 동영상 플레이어를 만들어보자(2) (0) | 2022.01.08 |
PyQt5를 이용하여 동영상 플레이어를 만들어보자(1) (0) | 2022.01.06 |
댓글