使用OpenCV与PyQt5播放视频时速度过快,如何实现正常帧率播放?
Hey there! Let's get your video playing at normal speed instead of that hyper-fast mode you're seeing. The core issue here is that your current code is pumping out frames as quickly as your CPU can process them—no regard for the video's actual frame rate. Those cv2.CAP_PROP_FPS and cv2.CAP_PROP_BUFFERSIZE settings don't help because you aren't using the frame rate to regulate the playback timing.
Why Your Current Code Isn't Working
Right now, your VideoThread.run() method just loops non-stop, reading frames and emitting them immediately. There's no delay between frames to match the video's intended speed. Even if you read the FPS value, you need to use it to calculate how long to wait between each frame update.
The Fix: Add Timing Control & Safe Thread Termination
We'll make two key changes:
- Calculate the correct delay between frames using the video's actual FPS.
- Replace the unsafe
terminate()call with a proper thread stop flag to avoid memory leaks.
Here's the modified code with explanations:
import os import cv2 import numpy as np from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QThread class VideoThread(QThread): change_pixmap_signal = pyqtSignal(np.ndarray) def __init__(self, parent=None): super(VideoThread, self).__init__(parent) self.get_file_dir = self.parent().file_dir # Add a flag to safely control thread execution self.is_running = True def run(self): cap = cv2.VideoCapture(self.get_file_dir) # Get the video's actual frame rate fps = cap.get(cv2.CAP_PROP_FPS) # Calculate delay between frames in milliseconds frame_delay = int(1000 / fps) if fps > 0 else 30 # Fallback to 30fps if FPS is invalid while self.is_running: ret, cv_img = cap.read() if ret: self.change_pixmap_signal.emit(cv_img) # Wait for the calculated delay to match real playback speed QThread.msleep(frame_delay) else: # End of video: reset or break the loop break cap.release() def stop(self): # Safely stop the thread by setting the flag self.is_running = False self.wait() class Ui_MainWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): super(Ui_MainWindow, self).__init__(parent) self.setupUI() # Initialize thread variable to avoid AttributeError self.thread = None def setupUI(self): MainWindow.resize(1720, 863) self.centralwidget = QtWidgets.QWidget(MainWindow) self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget) self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) self.tabWidget.setTabPosition(QtWidgets.QTabWidget.North) self.tabWidget.setTabShape(QtWidgets.QTabWidget.Triangular) self.tabWidget.setElideMode(QtCore.Qt.ElideNone) self.tabWidget.setTabsClosable(False) self.tabWidget.setMovable(True) self.tab = QtWidgets.QWidget() self.videoimage = QtWidgets.QLabel(self.tab) self.videoimage.setGeometry(QtCore.QRect(0, 10, 1280, 720)) self.listWidget = QtWidgets.QListWidget(self.tab) self.listWidget.setGeometry(QtCore.QRect(1290, 60, 391, 241)) self.listWidget.setObjectName("listWidget") self.listWidget.itemClicked.connect(self.itemclick) self.tabWidget.addTab(self.tab, "") self.tabWidget.setTabText(0, "Testing") self.verticalLayout_2.addWidget(self.tabWidget) MainWindow.setCentralWidget(self.centralwidget) self.listvideos() self.tabWidget.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(MainWindow) @pyqtSlot(np.ndarray) def listvideos(self): file_extension = ['avi','mp4','mov'] arr = os.listdir('D:\\Files\\VSCODE\\APPS\\HDTS\\videos') for i in arr: for fe in file_extension: if i.lower().endswith(fe): # Use lower() to handle case-insensitive extensions item = QtWidgets.QListWidgetItem(str(i)) self.listWidget.addItem(item) def itemclick(self): # Stop existing thread safely if it's running if self.thread is not None and self.thread.isRunning(): self.thread.stop() item = self.listWidget.currentItem() self.file_dir = 'D:\\Files\\VSCODE\\APPS\\HDTS\\videos\\' + item.text() print(self.file_dir) self.thread = VideoThread(self) self.thread.change_pixmap_signal.connect(self.update_image) self.thread.start() def update_image(self, cv_img): qt_img = self.convert_cv_qt(cv_img) self.videoimage.setPixmap(qt_img) def convert_cv_qt(self, cv_img): rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) p = convert_to_Qt_format.scaled(1280, 720, Qt.KeepAspectRatio) return QtGui.QPixmap.fromImage(p) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() MainWindow.show() sys.exit(app.exec_())
Key Changes Explained:
- Thread Safety: Added
self.is_runningflag andstop()method to safely halt the thread instead of usingterminate()(which can leave resources like the video capture open). - Frame Timing: Calculated
frame_delaybased on the video's FPS, then usedQThread.msleep(frame_delay)to pause between frames—this ensures each frame stays on screen for the correct amount of time. - Extension Check: Updated the file extension check to use
endswith()andlower()to catch filenames with uppercase extensions (like.MP4). - Thread Initialization: Added
self.thread = Noneto avoid AttributeErrors when stopping a thread that hasn't been created yet.
Bonus Tips:
- If you notice slight jitter, you can adjust the delay calculation or use
QThread.usleep()for microsecond precision. - For better performance, consider resizing frames in the thread instead of the
convert_cv_qtmethod—this offloads work from the main UI thread.
内容的提问来源于stack exchange,提问作者gentales17

