为什么我的 PyQt5 和 Vispy 应用程序仅在 QThread 循环中添加睡眠时更新 GUI?

问题描述 投票:0回答:1

基本上我正在尝试使用 Vispy 库实现 PyQt5 应用程序以进行实时数据可视化。在下面的代码中,我尝试绘制一个每秒 10000 个样本、持续 100 秒的正弦波。我在 QThread 上运行数据创建/更新,并在主线程上运行图形的 GUI 更新。但是,运行代码会导致 GUI 冻结并且不会在指定的时间间隔内更新。

from PyQt5.QtGui import QCloseEvent
from vispy.app import use_app, Timer
import numpy as np
from PyQt5 import QtWidgets, QtCore
from vispy import scene, visuals
import time
from scipy import signal
from collections import deque
import cProfile
import pstats


class MainWindow(QtWidgets.QMainWindow):
    closing = QtCore.pyqtSignal()
    def __init__(self, canvas_wrapper, *args, **kwargs):

    
        super(MainWindow, self).__init__(*args, **kwargs)
        
        central_widget = QtWidgets.QWidget()
        main_layout = QtWidgets.QHBoxLayout()

        
        self.canvas_wrapper = canvas_wrapper

        main_layout.addWidget(self.canvas_wrapper.canvas.native)

        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)
    
    def closeEvent(self, event):
        print("Closing main window")
        self.closing.emit()
        return super().closeEvent(event)


class CanvasWrapper:
    def __init__(self, update_interval = .016): 

        self.canvas = scene.SceneCanvas(keys='interactive', size=(600, 600), show=True)
        self.grid = self.canvas.central_widget.add_grid()

        title = scene.Label("Test Plot", color='white')
        title.height_max = 40

        self.grid.add_widget(title, row=0, col=0, col_span=2)

        self.yaxis = scene.AxisWidget(orientation='left',
                         axis_label='Y Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=10)
        
        self.yaxis.width_max = 80
        self.grid.add_widget(self.yaxis, row=1, col=0)

        self.xaxis = scene.AxisWidget(orientation='bottom',
                         axis_label='X Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=20)

        self.xaxis.height_max = 80
        self.grid.add_widget(self.xaxis, row=2, col=1)
        
        right_padding = self.grid.add_widget(row=1, col=2, row_span=1)
        right_padding.width_max = 50

        self.view = self.grid.add_view(row=1, col=1, border_color='white')
        self.view.camera = "panzoom"
        
        self.data = np.empty((2, 2))
        self.line = scene.Line(self.data, parent=self.view.scene)


        self.xaxis.link_view(self.view)
        self.yaxis.link_view(self.view)

        self.update_interval = update_interval
        self.last_update_time = time.time()


    def update_data(self, newData):
        if self.should_update():

            data_array = newData["data"]
            data_array = np.array(data_array)

            x_min, x_max = data_array[:, 0].min(), data_array[:, 0].max()
            y_min, y_max = data_array[:, 1].min(), data_array[:, 1].max()
            
           
            self.view.camera.set_range(x=(x_min, x_max), y=(y_min, y_max))
            self.line.set_data(data_array)

            
    
    def should_update(self):
        current_time = time.time()
        if current_time - self.last_update_time >= self.update_interval:
            self.last_update_time = current_time
            return True
        return False


class DataSource(QtCore.QObject):
    new_data = QtCore.pyqtSignal(dict)
    finished = QtCore.pyqtSignal()

    def __init__(self, sample_rate, seconds, seconds_to_display=15, q = 100, parent = None):
        super().__init__(parent)
        self.count = 0
        self.q = q
        self.should_end = False
        self.sample_rate = sample_rate

        self.num_samples = seconds*sample_rate
        self.seconds_to_display = seconds_to_display

        size = self.seconds_to_display*self.sample_rate

        self.buffer = deque(maxlen=size)
        


    def run_data_creation(self):
        print("Run Data Creation is starting")
        for count in range (self.num_samples):
            if self.should_end:
                print("Data saw it was told to end")
                break
        
            self.update(self.count)
            self.count += 1

            data_dict = {
                "data": self.buffer,
            }
            self.new_data.emit(data_dict)

        print("Data source finished")
        self.finished.emit()
    
    def stop_data(self):
        print("Data source is quitting...")
        self.should_end = True

    def update(self, count):

        x_value = count / self.sample_rate
        y_value = np.sin((count / self.sample_rate) * np.pi)

        self.buffer.append([x_value, y_value])
        

class Main:
    def __init__(self, sample_rate, seconds, seconds_to_display):
        # Set up the application to use PyQt5
        self.app = use_app("pyqt5")
        self.app.create()

        # Set some parameters for the data source
        self.sample_rate, self.seconds, self.seconds_to_display = sample_rate, seconds, seconds_to_display

        # Create the canvas wrapper and main window
        self.canvas_wrapper = CanvasWrapper()
        self.win = MainWindow(self.canvas_wrapper)

        # Set up threading for the data source
        self.data_thread = QtCore.QThread(parent=self.win)
        self.data_source = DataSource(self.sample_rate, self.seconds)

        # Move the data source to the thread
        self.data_source.moveToThread(self.data_thread)

        # Connect signals and slots for data updates and thread management
        self.setup_connections()

    def setup_connections(self):
        self.data_source.new_data.connect(self.canvas_wrapper.update_data)
        self.data_thread.started.connect(self.data_source.run_data_creation)
        self.data_source.finished.connect(self.data_thread.quit, QtCore.Qt.DirectConnection)
        self.win.closing.connect(self.data_source.stop_data, QtCore.Qt.DirectConnection)
        self.data_thread.finished.connect(self.data_source.deleteLater)

    def run(self):
        self.win.show()
        self.data_thread.start()
        
        self.app.run()
        self.data_thread.quit()
        self.data_thread.wait(5000)


def profile_run():
    visualization_app = Main(10000, 100, 10)
    visualization_app.run()

if __name__ == "__main__":
    cProfile.run('profile_run()', 'profile_out')
    stats = pstats.Stats('profile_out')

    stats.sort_stats('cumulative')

    stats.print_stats(10)

    stats.sort_stats('time').print_stats(10)

只有当我在 run_data_creation 中添加睡眠时,GUI 才会更新(尽管即使睡眠时间很短,速度也很慢)。为什么QThread要这样做呢?我该如何解决它?

    def run_data_creation(self):
        print("Run Data Creation is starting")
        for count in range (self.num_samples):
            if self.should_end:
                print("Data saw it was told to end")
                break
        
            self.update(self.count)
            self.count += 1
            time.sleep(.0000001)

            data_dict = {
                "data": self.buffer,
            }
            self.new_data.emit(data_dict)

        print("Data source finished")
        self.finished.emit()
    
python multithreading thread-safety vispy qtpy
1个回答
0
投票

答案2

如果您注释掉

update_data
中的两条 vispy 行并可选择添加一些打印,您会注意到 GUI 的行为仍然相同。那就是它冻结了。所以这与vispy无关。

作为实时应用程序的一般规则,任何非可视化操作都应该在单独的线程(数据源)中。这包括您的更新间隔逻辑。换句话说,仅在可视化应该更新时发送数据。

就 Qt 事件循环而言,您基本上将数千个事件转储到 Qt 必须处理的任务队列中。 Qt 在完成数据事件处理之前无法处理任何新的 UI 事件。这基本上最终成为 python 中数千个事件的

for
循环。

解决方案(至少从一开始)是在数据源线程内处理所有更新间隔的内容。仅当数据准备好显示时才发出信号。另外,如果您的目标是每秒 10000 个点,则需要实时查看大量数据。您可能需要对数据进行平均或子集化,以减少发送更新数据的频率。

答案1

看起来这是基于vispy实时数据示例,对吗?当我编写这些内容时,我不记得我对非睡眠数据源做了多少测试,因此该代码总是有可能不按我的预期运行。

您提到这不是在“指定的时间间隔”内更新,您这是什么意思?可能是您的 GUI 中充斥着如此多的更新事件,以至于在您看到最终结果之前它无法更新。如果没有睡眠(如果我错了,请纠正我),您基本上会以 CPU 所能达到的最快速度从第一次迭代到最后一次迭代,对吧?在这种情况下,您希望看到什么?在更现实的示例中,数据源创建数据将花费一些实际时间,但您的示例会立即创建所有数据。您确定应用程序挂起还是刚刚完成生成所有数据?

如果我对上述所有内容都错了,那么我看到的一个区别是您对

deque
的使用,我对此几乎没有经验。我想知道如果您在每次迭代的数据源中创建一个新的 numpy 数组,您是否会看到行为上的任何差异。

© www.soinside.com 2019 - 2024. All rights reserved.