0. 概要
本文利用 PyQt5 和 OpenGL 提供了良好的用户界面和流畅的视频渲染效果;使用线程锁确保了多线程操作的安全性,同时在每帧视频上叠加了简单的图形和文字,展示了 OpenCV 在视频处理中的应用。
1. 说明
- 支持h264文件播放模块,在h264文件模块添加帧总数;
- 需要搭配时间戳文件(time_stamp.txt)使用,以方便显示播放进度;
- 有播放、暂停和停止功能;
- 使用opengl渲染;
- 有轨迹叠加演示;
- 仓库代码: pyqt5-pyopengl-opencv-example。
2. 主要代码
# -*- coding: utf-8 -*-
# ref: https://www.cnblogs.com/superxuezhazha/p/6195328.html
# ref: https://riptutorial.com/zh-CN/pyqt5/example/29500/%E5%9F%BA%E6%9C%AC%E7%9A%84pyqt%E8%BF%9B%E5%BA%A6%E6%9D%A1
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtOpenGL import *
from OpenGL.GL import *
import sys
import time
import struct
import logging
import cv2
import numpy as np
VIDEO_WIDTH = 512
VIDEO_HEIGHT = 424
playerMutex = QMutex() # 创建线程锁
# 底部控制条,开始、暂停、进度条等
class PlayerControl(QDialog):
videoPlayStarted = pyqtSignal()
videoPlayPaused = pyqtSignal()
videoPlayStoped = pyqtSignal()
def __init__(self):
super().__init__()
self.init_ui()
self.init_event()
self.thread_pause = False
def init_ui(self):
self.setWindowTitle('Progress Bar')
self.setFixedHeight(80)
self.progress = QProgressBar(self)
self.progress.setGeometry(0, 0, 500, 25)
self.progress.setMaximum(100)
self.buttonStart = QPushButton('Start', self)
self.buttonPause = QPushButton('Pause', self)
self.buttonStop = QPushButton('Stop', self)
self.buttonStart.move(0, 30)
self.buttonPause.move(90, 30)
self.buttonStop.move(180, 30)
def init_event(self):
self.buttonStart.clicked.connect(self.onButtonStartClick)
self.buttonPause.clicked.connect(self.onButtonPauseClick)
self.buttonStop.clicked.connect(self.onButtonStopClick)
def onButtonStartClick(self):
if self.thread_pause:
playerMutex.unlock()
self.thread_pause = False
self.buttonStart.setEnabled(False)
self.buttonPause.setEnabled(True)
self.buttonStop.setEnabled(True)
self.videoPlayStarted.emit()
def onButtonPauseClick(self):
playerMutex.lock()
self.thread_pause = True
self.buttonStart.setEnabled(True)
self.buttonPause.setEnabled(False)
self.buttonStop.setEnabled(True)
self.videoPlayPaused.emit()
def onButtonStopClick(self):
self.progress.setValue(0)
if self.thread_pause:
playerMutex.unlock()
self.thread_pause = False
self.buttonStart.setEnabled(True)
self.buttonPause.setEnabled(False)
self.buttonStop.setEnabled(False)
self.videoPlayStoped.emit()
def onProcessChanged(self, value):
self.progress.setValue(value)
class VideoPlayer(QThread):
"""
Runs a VideoPlayer thread.
"""
updateView = pyqtSignal(bytes, int, int)
progressChanged = pyqtSignal(int)
def __init__(self, video_file):
super(VideoPlayer, self).__init__()
self._run = False
self.video_pause = True
self.video_Stop = True
self.video_file = video_file
def get_frame_count(self):
# 自定义格式,在视频末尾增加了帧总数,8个字节
with open(self.video_file, "rb") as f:
f.seek(f.seek(0, 2) - 8, 0)
frame_count = struct.unpack('Q', f.read(8))[0]
logging.info("frame_count: %s" %frame_count)
return frame_count
def onVideoPlayStarted(self):
self.video_pause = False
self.video_Stop = False
self.start()
def onVideoPlayPaused(self):
self.video_pause = True
self.video_Stop = False
def onVideoPlayStoped(self):
self.video_pause = True
self.video_Stop = True
self._run = False
def run(self):
cap = cv2.VideoCapture(self.video_file)
if not cap:
return
frame_index = 0
total_frame_num = self.get_frame_count()
self._run = True
while self._run:
if self.video_pause:
playerMutex.lock()
self.video_pause = False
playerMutex.unlock()
continue
ret, frame = cap.read()
if not ret:
continue
if self.video_Stop:
break
if total_frame_num and total_frame_num > 0:
frame_index += 1
progress = frame_index*100 / total_frame_num
self.progressChanged.emit(progress)
img = cv2.resize(frame, (VIDEO_WIDTH, VIDEO_HEIGHT), interpolation = cv2.INTER_AREA)
# 用绿色(0, 255, 0)来画出矩形
x, y, w, h = 10, 10, 200, 300
img = cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
# Using cv2.putText() method
img = cv2.putText(img, 'OpenCV', (50, 50), cv2.FONT_HERSHEY_SIMPLEX , 1, (255, 0, 0), 2, cv2.LINE_AA)
# Our operations on the frame come here
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_data = rgb.tobytes()
if self.video_Stop: # 防止此时已经退出
break
# Display the resulting frame
self.updateView.emit(img_data, rgb.shape[0], rgb.shape[1])
cap.release()
def stop(self):
self._run = False
# 播放视频的opengl窗口
class VideoView(QGLWidget):
def __init__(self, parent=None):
super(VideoView, self).__init__(parent)
self.width, self.height = VIDEO_WIDTH, VIDEO_HEIGHT
self.texture, self.data = None, None
if parent:
self.resize(parent.size())
def update_view(self, data, height, width):
self.data = data
self.height = height
self.width = width
self.updateGL()
def initializeGL(self):
glClearColor(0.5, 0.5, 0.5, 1.0)
glEnable(GL_DEPTH_TEST)
glClearColor(0.0, 0.0, 0.0, 0.0)
glClearDepth(2000.0)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable(GL_TEXTURE_2D)
self.texture = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.texture)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
def resizeGL(self, width, height):
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, self.width, self.height, 0.0, 0.0, 1.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def paintGL(self):
if self.data is not None:
glClearColor(.5, .5, .5, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# , width = self.data.shape[:2]
glTexImage2D(GL_TEXTURE_2D,
0,
3,
self.width,
self.height,
0,
GL_RGB,
GL_UNSIGNED_BYTE,
self.data)
glLoadIdentity()
glEnable(GL_TEXTURE_2D)
glBegin(GL_TRIANGLE_FAN)
glColor4f(255.0, 255.0, 255.0, 255.0)
glTexCoord2f(0, 0); glVertex3f(0, 0, 0)
glTexCoord2f(1, 0); glVertex3f(self.width, 0, 0)
glTexCoord2f(1, 1); glVertex3f(self.width, self.height, 0)
glTexCoord2f(0, 1); glVertex3f(0, self.height, 0)
glEnd()
# 组合播放窗口和进度条
class CustomVideoPlayer(QDialog):
def __init__(self, video_file):
super().__init__()
self.setFixedSize(900, 600)
self.videoView = VideoView()
self.playerControl = PlayerControl()
self.videoPlayer = VideoPlayer(video_file)
self.init_event()
self.init_ui()
def init_event(self):
self.playerControl.videoPlayStarted.connect(self.videoPlayer.onVideoPlayStarted)
self.playerControl.videoPlayPaused.connect(self.videoPlayer.onVideoPlayPaused)
self.playerControl.videoPlayStoped.connect(self.videoPlayer.onVideoPlayStoped)
self.videoPlayer.progressChanged.connect(self.playerControl.onProcessChanged)
self.videoPlayer.updateView.connect(self.videoView.update_view)
def init_ui(self):
self.v_box = QVBoxLayout()
self.v_box.addWidget(self.videoView)
self.v_box.addWidget(self.playerControl)
self.setLayout(self.v_box)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = CustomVideoPlayer('camera2.h264')
window.show()
sys.exit(app.exec_())
3. 代码解释和分析
这段代码实现了一个基于 PyQt5 和 OpenGL 的视频播放器,具备播放、暂停、停止功能,并支持视频帧的显示和轨迹叠加。下面是对其主要功能和实现的解释和分析:
-
功能概述:
- 视频播放: 使用
VideoPlayer
类来读取指定的 h264 视频文件,并根据时间戳文件(未在代码中显示)显示播放进度。 - 控制条:
PlayerControl
类提供了开始、暂停、停止按钮以及进度条,用于控制视频的播放和显示当前进度。 - OpenGL 渲染:
VideoView
类使用 OpenGL 渲染每一帧视频,确保流畅的显示效果。 - 轨迹叠加: 在每一帧视频上叠加绿色矩形和文字,演示了 OpenCV 的基本用法。
- 视频播放: 使用
-
代码结构:
- 主程序入口:
CustomVideoPlayer
类作为程序的入口点,创建了视频播放窗口、视频播放器、控制条,并将它们组合在一起。 - 线程安全: 使用
playerMutex
确保在播放暂停时的线程安全性,避免多线程冲突。 - 信号与槽机制: 使用 PyQt5 的信号与槽机制,实现了组件之间的通信,例如控制条按钮操作与视频播放器的状态变更。
- 主程序入口:
-
Mermaid 流程图:
下面是使用 Mermaid 语法绘制的流程图,展示了程序的主要流程和组件交互过程:
这个流程图简要展示了主程序、控制条、视频显示和处理的关系,帮助理解整体架构和数据流动。