1. 동영상 재생 중 삭제 구현하기
쓰레드를 이용하여 opencv로 불러낸 동영상의 재생을 제어해 보자.
이젠 동영상 재생 중 삭제가 가능하다. (아직 일시정지 구현 전이다.)
import sys, cv2
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import time
from queue import Queue
from threading import Thread
def open_movie():
print("동영상 불러오기를 성공하였습니다.")
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("physics Lab")
self.setGeometry(100, 100, 1600, 800)
self.setWindowIcon(QIcon("physics.png"))
# 상태바 생성
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.vid_label = QLabel(parent=self)
self.vid_label.move(10, 10)
self.btn = QPushButton(text = "동영상 열기", parent = self)
self.btn.move(1500, 10)
self.btn.clicked.connect(self.btn_clicked)
self.btn2 = QPushButton(text = "재생 ▶", parent = self)
self.btn2.move(1500, 50)
self.btn2.clicked.connect(self.btn2_clicked)
self.btn3 = QPushButton(text = "일시정지 ||", parent = self)
self.btn3.move(1500, 90)
self.btn3.clicked.connect(self.btn3_clicked)
self.btn4 = QPushButton(text = "동영상 삭제", parent = self)
self.btn4.move(1500, 130)
self.btn4.clicked.connect(self.btn4_clicked)
self.cap = None # 동영상 재생 객체
self.is_playing = False # 재생 확인 객체
self.queue = Queue() # 동영상 프레임을 담을 큐
self.worker_thread = None # 쓰레드 객체 정의
def btn_clicked(self):
fname, _ = QFileDialog.getOpenFileName(self, "Choose File", "", "All files(*)")
if not fname:
self.status_bar.showMessage("파일 선택이 취소되었습니다.")
return
self.cap = cv2.VideoCapture(fname) # 동영상 갭쳐 객체 생성
width = self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.vid_label.resize(int(width), int(height))
if self.cap is None: # 이미지가 제대로 읽히지 않은 경우
self.status_bar.showMessage("동영상을 불러오는 데 실패하였습니다.")
return # 에러 처리 후 함수 종료
_, img = self.cap.read() # 프레임 읽기
self.queue.put(img)
self.disp_video() #비디오 첫 화면만 보여주기
self.status_bar.showMessage("동영상을 불러오는 데 성공하였습니다.")
def btn2_clicked(self):
if self.worker_thread is None or not self.worker_thread.is_alive():
self.is_playing = True
self.worker_thread = Thread(target=self.thread_worker, daemon=True)
self.worker_thread.start()
def btn3_clicked(self):
pass
def btn4_clicked(self):
self.vid_label.clear()
self.status_bar.showMessage("동영상을 삭제하였습니다.")
self.is_playing = False # 재생 확인 객체
if self.worker_thread is not None:
self.worker_thread.join()
def thread_worker(self):
while True:
ret, img = self.cap.read() # 프레임 읽기
self.queue.put(img)
if ret and not self.queue.empty() and self.is_playing:
self.disp_video() #비디오 재생하기
cv2.waitKey(int(self.cap.get(cv2.CAP_PROP_FPS)))
self.status_bar.showMessage("동영상을 재생중입니다.")
else:
self.status_bar.showMessage("동영상 재생을 중지합니다.")
break
self.cap.release()
def disp_video(self):
image = self.queue.get()
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, c = image.shape
qImg = QImage(image.data, w, h, w*c, QImage.Format_RGB888)
self.pixmap = QPixmap.fromImage(qImg)
self.vid_label.setPixmap(self.pixmap)
QApplication.processEvents()
app = QApplication(sys.argv)
win = MyWindow()
win.show()
app.exec_()
버튼을 눌렀을 때 실행하는 콜백 함수들의 역할을 중심으로 알아보자.
1. btn_clicked(동영상 열기) 함수
def btn_clicked(self):
fname, _ = QFileDialog.getOpenFileName(self, "Choose File", "", "All files(*)")
if not fname:
self.status_bar.showMessage("파일 선택이 취소되었습니다.")
return
self.cap = cv2.VideoCapture(fname) # 동영상 갭쳐 객체 생성
width = self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.vid_label.resize(int(width), int(height))
if self.cap is None: # 이미지가 제대로 읽히지 않은 경우
self.status_bar.showMessage("동영상을 불러오는 데 실패하였습니다.")
return # 에러 처리 후 함수 종료
_, img = self.cap.read() # 프레임 읽기
self.queue.put(img)
self.disp_video() #비디오 첫 화면만 보여주기
self.status_bar.showMessage("동영상을 불러오는 데 성공하였습니다.")
- 역할: 동영상 파일을 선택하고, 해당 동영상을 불러와서 첫 번째 프레임을 표시하는 기능을 한다.
- 동작 과정:
1. QFileDialog.getOpenFileName을 호출하여 사용자가 동영상 파일을 선택하도록 한다.
2. 파일 선택이 취소되면 상태바에 취소 메시지를 표시하고 함수를 종료한다.
3. cv2.VideoCapture를 사용하여 선택한 파일로 동영상 캡처 객체를 생성한다
4. 동영상의 너비와 높이를 가져와서 vid_label의 크기를 조정한다.
5. 동영상이 열리지 않은 경우, 상태바에 오류 메시지를 표시한다.
6. 첫 번째 프레임을 읽고 큐에 넣은 후, disp_video 함수를 호출하여 첫 화면을 표시한다.
7. 상태바에 성공 메시지를 표시한다.
2. btn2_clicked(동영상 재생) 함수
def btn2_clicked(self):
if self.worker_thread is None or not self.worker_thread.is_alive():
self.is_playing = True
self.worker_thread = Thread(target=self.thread_worker, daemon=True)
self.worker_thread.start()
- 역할: 동영상을 재생하는 기능을 담당한다.
- 동작 과정:
1. 현재 스레드가 실행 중이 아니거나 종료된 경우에만 동작한다.
2. is_playing 변수를 True로 설정하여 재생 상태로 변경한다.
3. Thread를 생성하여 thread_worker 함수를 실행하고, 이를 데몬 스레드로 설정하여 프로그램 종료 시 자동 종료되도록 한다.
3. btn4_clicked(동영상 삭제) 함수
def btn4_clicked(self):
self.vid_label.clear()
self.status_bar.showMessage("동영상을 삭제하였습니다.")
self.is_playing = False # 재생 확인 객체
if self.worker_thread is not None:
self.worker_thread.join()
- 역할: 동영상 뷰어를 초기화하고 동영상을 삭제하는 기능을 한다.
- 동작 과정:
1. vid_label을 지워서 화면에서 동영상을 제거한다.
2. 상태바에 "동영상을 삭제하였습니다."라는 메시지를 표시한다.
3. is_playing 변수를 False로 설정하여 재생 상태를 종료한다.
4. 만약 worker_thread가 존재하면 join() 메서드를 호출하여 해당 스레드가 종료될 때까지 기다린다.
4. thread_worker 함수
def thread_worker(self):
while True:
ret, img = self.cap.read() # 프레임 읽기
self.queue.put(img)
if ret and not self.queue.empty() and self.is_playing:
self.disp_video() #비디오 재생하기
cv2.waitKey(int(self.cap.get(cv2.CAP_PROP_FPS)))
self.status_bar.showMessage("동영상을 재생중입니다.")
else:
self.status_bar.showMessage("동영상 재생을 중지합니다.")
break
self.cap.release()
- 역할: 동영상의 프레임을 지속적으로 읽고 재생하는 스레드의 메인 로직을 담당한다.
- 동작 과정:
1. 무한 루프를 통해 동영상을 반복적으로 읽는다.
2. 각 프레임을 큐에 넣는다.
3. is_playing이 True이고 큐가 비어있지 않은 경우에만 프레임을 읽어 disp_video를 호출하여 비디오를 재생한다.
4. 각 프레임을 읽은 후, cv2.waitKey를 호출하여 설정된 FPS에 맞게 대기한다.
5. 프레임 읽기가 실패하면 상태바에 "동영상 재생을 중지합니다."라는 메시지를 표시하고 루프를 종료한다.
6. 모든 재생이 끝난 후, cap.release()를 호출하여 동영상 캡처 객체를 해제한다.
5. disp_video 함수
def disp_video(self):
image = self.queue.get()
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, c = image.shape
qImg = QImage(image.data, w, h, w*c, QImage.Format_RGB888)
self.pixmap = QPixmap.fromImage(qImg)
self.vid_label.setPixmap(self.pixmap)
QApplication.processEvents()
- 역할: 큐에서 프레임을 가져와서 QLabel에 표시하는 기능을 한다.
- 동작 과정:
1. 큐에서 이미지를 꺼내온다.
2. OpenCV에서 사용하는 BGR 포맷의 이미지를 RGB 포맷으로 변환한다.
3. 이미지를 QImage로 변환하여 QPixmap으로 변환한다.
4. vid_label에 QPixmap을 설정하여 프레임을 화면에 표시한다.
5. QApplication.processEvents()를 호출하여 GUI가 즉시 업데이트되도록 한다.
2. 동영상 일시정지 구현
이번엔 재생 중 일시 정지를 구현하자
import sys
import cv2
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from queue import Queue
from threading import Thread
import time
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("physics Lab")
self.setGeometry(100, 100, 1600, 800)
self.setWindowIcon(QIcon("physics.png"))
# 상태바 생성
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.vid_label = QLabel(parent=self)
self.vid_label.move(10, 10)
self.btn = QPushButton(text="동영상 열기", parent=self)
self.btn.move(1500, 10)
self.btn.clicked.connect(self.btn_clicked)
self.btn2 = QPushButton(text="재생 ▶", parent=self)
self.btn2.move(1500, 50)
self.btn2.clicked.connect(self.btn2_clicked)
self.btn3 = QPushButton(text="일시정지 ||", parent=self)
self.btn3.move(1500, 90)
self.btn3.clicked.connect(self.btn3_clicked)
self.btn4 = QPushButton(text="동영상 삭제", parent=self)
self.btn4.move(1500, 130)
self.btn4.clicked.connect(self.btn4_clicked)
self.cap = None # 동영상 재생 객체
self.is_playing = False # 재생 확인 객체
self.is_paused = False # 일시정지 상태 확인
self.queue = Queue() # 동영상 프레임을 담을 큐
self.worker_thread = None # 쓰레드 객체 정의
def btn_clicked(self):
fname, _ = QFileDialog.getOpenFileName(self, "Choose File", "", "All files(*)")
if not fname:
self.status_bar.showMessage("파일 선택이 취소되었습니다.")
return
self.cap = cv2.VideoCapture(fname) # 동영상 갭쳐 객체 생성
width = self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.vid_label.resize(int(width), int(height))
if not self.cap.isOpened(): # 동영상이 제대로 열리지 않은 경우
self.status_bar.showMessage("동영상을 불러오는 데 실패하였습니다.")
return # 에러 처리 후 함수 종료
ret, img = self.cap.read() # 첫 번째 프레임 읽기
if ret:
self.queue.put(img) # 첫 번째 프레임을 큐에 넣음
self.disp_video() # 비디오 첫 화면만 보여주기
self.status_bar.showMessage("동영상을 불러오는 데 성공하였습니다.")
else:
self.status_bar.showMessage("첫 프레임을 읽는 데 실패하였습니다.")
def btn2_clicked(self):
if self.worker_thread is None or not self.worker_thread.is_alive():
self.is_playing = True
self.is_paused = False # 재생 상태로 설정
self.worker_thread = Thread(target=self.thread_worker, daemon=True)
self.worker_thread.start()
def btn3_clicked(self):
if self.is_playing:
self.is_paused = not self.is_paused # 일시정지 상태 토글
if self.is_paused:
self.btn3.setText("다시 시작 ▶") # 버튼 텍스트 변경
self.status_bar.showMessage("동영상이 일시정지되었습니다.")
else:
self.btn3.setText("일시정지 ||") # 버튼 텍스트 원래대로 변경
self.status_bar.showMessage("동영상이 재생중입니다.")
def btn4_clicked(self):
self.is_playing = False
self.is_paused = False
self.vid_label.clear()
self.status_bar.showMessage("동영상을 삭제하였습니다.")
if self.worker_thread is not None:
self.worker_thread.join()
def thread_worker(self):
while self.is_playing:
if not self.is_paused: # 일시정지 상태가 아닐 때만 프레임을 읽음
ret, img = self.cap.read() # 프레임 읽기
if ret:
self.queue.put(img)
self.disp_video() # 비디오 재생하기
cv2.waitKey(int(self.cap.get(cv2.CAP_PROP_FPS))) # FPS에 맞춰 대기
else:
self.status_bar.showMessage("동영상 재생이 완료되었습니다.")
break
else:
time.sleep(0.1) # 일시정지 상태일 때 대기
self.cap.release()
def disp_video(self):
if not self.queue.empty():
image = self.queue.get()
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, c = image.shape
qImg = QImage(image.data, w, h, w*c, QImage.Format_RGB888)
self.pixmap = QPixmap.fromImage(qImg)
self.vid_label.setPixmap(self.pixmap)
QApplication.processEvents()
app = QApplication(sys.argv)
win = MyWindow()
win.show()
app.exec_()
1. btn_clicked(동영상 열기) 함수
def btn_clicked(self):
fname, _ = QFileDialog.getOpenFileName(self, "Choose File", "", "All files(*)")
if not fname:
self.status_bar.showMessage("파일 선택이 취소되었습니다.")
return
self.cap = cv2.VideoCapture(fname) # 동영상 갭쳐 객체 생성
width = self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.vid_label.resize(int(width), int(height))
if not self.cap.isOpened(): # 동영상이 제대로 열리지 않은 경우
self.status_bar.showMessage("동영상을 불러오는 데 실패하였습니다.")
return # 에러 처리 후 함수 종료
ret, img = self.cap.read() # 첫 번째 프레임 읽기
if ret:
self.queue.put(img) # 첫 번째 프레임을 큐에 넣음
self.disp_video() # 비디오 첫 화면만 보여주기
self.status_bar.showMessage("동영상을 불러오는 데 성공하였습니다.")
else:
self.status_bar.showMessage("첫 프레임을 읽는 데 실패하였습니다.")
- 역할: 동영상 파일을 선택하고, 해당 동영상을 불러와서 첫 번째 프레임을 표시하는 기능을 한다.
- 동작 과정:
1. QFileDialog.getOpenFileName을 호출하여 사용자가 동영상 파일을 선택하도록 한다.
2. 파일 선택이 취소되면 상태바에 취소 메시지를 표시하고 함수를 종료한다.
3. cv2.VideoCapture를 사용하여 선택한 파일로 동영상 캡처 객체를 생성한다.
4. 동영상의 너비와 높이를 가져와서 vid_label의 크기를 조정한다.
5. 동영상이 제대로 열리지 않은 경우, 상태바에 오류 메시지를 표시한다.
6. 첫 번째 프레임을 읽고 이를 큐에 넣은 후, disp_video 함수를 호출하여 첫 화면을 표시한다.
7. 상태바에 성공 메시지를 표시한다.
2. btn2_clicked(동영상 재생) 함수
def btn2_clicked(self):
if self.worker_thread is None or not self.worker_thread.is_alive():
self.is_playing = True
self.is_paused = False # 재생 상태로 설정
self.worker_thread = Thread(target=self.thread_worker, daemon=True)
self.worker_thread.start()
- 역할: 동영상을 재생하는 기능을 담당한다.
- 동작 과정:
1. 현재 스레드가 실행 중이 아니거나 종료된 경우에만 동작한다.
2. is_playing 변수를 True로 설정하여 재생 상태로 변경한다.
3. is_paused 변수를 False로 설정하여 일시정지 상태를 초기화한다.
4. 새로운 스레드를 생성하여 thread_worker 함수를 실행하고, 이를 데몬 스레드로 설정하여 프로그램 종료 시 자동으로 종료되도록 한다.
3. btn3_clicked(동영상 일시정지 및 다시재생) 함수
def btn3_clicked(self):
if self.is_playing:
self.is_paused = not self.is_paused # 일시정지 상태 토글
if self.is_paused:
self.btn3.setText("다시 시작 ▶") # 버튼 텍스트 변경
self.status_bar.showMessage("동영상이 일시정지되었습니다.")
else:
self.btn3.setText("일시정지 ||") # 버튼 텍스트 원래대로 변경
self.status_bar.showMessage("동영상이 재생중입니다.")
- 역할: 동영상의 일시정지 및 재생 상태를 전환하는 기능을 담당한다.
- 동작 과정:
1. 현재 동영상이 재생 중인 경우에만 동작한다.
2. is_paused 변수를 토글하여 일시정지 상태를 변경한다.
3. 일시정지 상태로 변경된 경우, 버튼 텍스트를 "다시 시작 ▶"로 변경하고 상태바에 일시정지 메시지를 표시한다.
4. 재생 상태로 돌아갈 경우, 버튼 텍스트를 "일시정지 ||"로 변경하고 상태바에 재생 중 메시지를 표시한다.
4. btn4_clicked(동영상 삭제) 함수
def btn4_clicked(self):
self.is_playing = False
self.is_paused = False
self.vid_label.clear()
self.status_bar.showMessage("동영상을 삭제하였습니다.")
if self.worker_thread is not None:
self.worker_thread.join()
- 역할: 동영상을 삭제하고, 관련된 상태를 초기화하는 기능을 담당한다.
- 동작 과정:
1. is_playing과 is_paused 변수를 False로 설정하여 재생 상태와 일시정지 상태를 초기화한다.
2. vid_label을 지워서 화면에서 동영상을 제거한다.
3. 상태바에 "동영상을 삭제하였습니다."라는 메시지를 표시한다.
4. 만약 worker_thread가 존재하면 join() 메서드를 호출하여 해당 스레드가 종료될 때까지 기다린다.
5. thread_worker 함수
def thread_worker(self):
while self.is_playing:
if not self.is_paused: # 일시정지 상태가 아닐 때만 프레임을 읽음
ret, img = self.cap.read() # 프레임 읽기
if ret:
self.queue.put(img)
self.disp_video() # 비디오 재생하기
cv2.waitKey(int(self.cap.get(cv2.CAP_PROP_FPS))) # FPS에 맞춰 대기
else:
self.status_bar.showMessage("동영상 재생이 완료되었습니다.")
break
else:
time.sleep(0.1) # 일시정지 상태일 때 대기
self.cap.release()
- 역할: 동영상의 프레임을 지속적으로 읽고 재생하는 스레드의 메인 로직을 담당한다.
- 동작 과정:
1. is_playing이 True인 동안 무한 루프를 실행한다.
2. is_paused가 False인 경우에만 프레임을 읽고, 프레임을 큐에 넣는다.
3. 프레임을 성공적으로 읽은 경우, disp_video를 호출하여 비디오를 재생하고, 설정된 FPS에 맞춰 대기한다.
4. 프레임 읽기가 실패하면 상태바에 "동영상 재생이 완료되었습니다."라는 메시지를 표시하고 루프를 종료한다.
5. 모든 재생이 끝난 후, cap.release()를 호출하여 동영상 캡처 객체를 해제한다.
6. disp_video 함수
def disp_video(self):
if not self.queue.empty():
image = self.queue.get()
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, c = image.shape
qImg = QImage(image.data, w, h, w*c, QImage.Format_RGB888)
self.pixmap = QPixmap.fromImage(qImg)
self.vid_label.setPixmap(self.pixmap)
QApplication.processEvents()
- 역할: 큐에서 프레임을 가져와서 QLabel에 표시하는 기능을 담당한다.
- 동작 과정:
1. 큐에서 이미지를 꺼내온다.
2. OpenCV에서 사용하는 BGR 포맷의 이미지를 RGB 포맷으로 변환한다.
3. 이미지를 QImage로 변환하여 QPixmap으로 변환한다.
4. vid_label에 QPixmap을 설정하여 프레임을 화면에 표시한다.
5. QApplication.processEvents()를 호출하여 GUI가 즉시 업데이트되도록 한다.
'파이썬 프로그래밍 > PyQt5 공부하기' 카테고리의 다른 글
7. PyQt5 쓰레드 사용하기 (0) | 2024.11.25 |
---|---|
6. PyQt5와 opencv 연동하기 : 동영상 불러오기 (0) | 2024.11.24 |
5. PyQt5와 opencv 연동하기 : 이미지 불러오기 (0) | 2024.11.21 |
4. PyQt5 기초(Box배치, Grid배치) (0) | 2024.11.11 |
3. PyQt5 기초(툴바, 스타일) (0) | 2024.11.10 |