在 PyQtGraph 中绘制大型时间序列时使用预下采样数据

Using pre-downsampled data when plotting large time series in PyQtGraph

我需要在 PyQtGraph 中绘制大型时间序列(数百万个点)。按原样绘制它几乎是不可能的,并且在打开优化选项时(使用 setDownsampling 进行下采样并使用 setClipToView 进行裁剪)缩小时它仍然几乎不可用(只有在放大时由于裁剪而变得很快).

不过我有一个想法。我可以预先对我的数据进行下采样,因为它们是静态的。然后,我可以在缩小时使用缓存的下采样数据,在放大时使用原始数据。

我怎样才能做到这一点?

我在一个名为 runviewer 的项目中做过类似的事情。一般的想法是,只要绘图的 x 范围发生变化,就对数据重新采样。我们使用的大概方法是:

  • 将一个方法连接到 PlotWidgetsigXRangeChanged 信号,它设置一个布尔标志,指示数据需要重新采样。

  • 启动一个线程,每隔 x 秒(我们选择 0.5 秒)轮询一次布尔标志,以查看是否需要对数据进行重采样。如果是,将使用您选择的算法(我们用 C 语言编写了自己的算法)对数据进行重新采样。然后将此数据发回主线程(例如使用 QThread 并向主线程发出信号),在主线程中调用 pyqtgraph 以更新图中的数据(注意,您只能调用来自主线程的 pyqtgraph 方法!)

我们使用布尔标志将 x 范围变化事件与重采样分离。您不希望每次 x 范围发生变化时都重新采样,因为当您使用鼠标缩放时信号会被多次触发,并且您不希望生成重新采样调用队列,因为重新采样很慢,即使使用 C !

您还需要确保重采样线程在检测到布尔标志为真时立即将布尔标志设置为假,然后运行重采样算法。这样当前重采样期间的后续 x 范围更改事件会导致后续重采样。

您也可以通过不轮询标志,而是使用某种线程来改进这一点 Event/Condition。

请注意,使用 Python 进行重采样非常非常慢,这就是我们选择编写重采样算法 C 并从 Python 调用它的原因。 numpy 主要在 C 中,所以会很快。但是我认为他们没有保留重采样算法的功能。大多数重采样人员所做的只是标准的下采样,即每第 N 个点取一次,但我们希望在缩小时仍然能够看到小于采样大小的特征的存在。


对性能的补充评论

我怀疑pyqtgraph内置方法的部分性能问题是降采样是在主线程中完成的。因此,必须在图形再次响应用户输入之前完成下采样。我们的方法避免了这种情况。我们的方法还限制了下采样发生的次数,最多每 the length of time it takes to down-sample + the poll delay 秒一次。因此,对于我们使用的延迟,我们仅每 0.5-1 秒进行一次下采样,同时保持主线程(因此 UI)响应。这确实意味着如果用户快速放大,他们可能会看到粗略采样的数据,但这在最多 2 次重采样迭代中得到纠正(因此最多延迟 1-2 秒)。此外,由于纠正时间很短,因此使用新采样数据的 updating/redrawing 通常是在用户完成与 UI 的交互后完成的,因此他们不会注意到期间有任何无响应重绘。

显然,我引用的次数完全取决于重采样的速度和轮询延迟!

@three_pineapples 的回答描述了对 PyQtGraph 中默认下采样的一个非常好的改进,但它仍然需要动态执行下采样,这在我的例子中是有问题的。

因此,我决定实施一种不同的策略,即对数据进行预下采样,然后 select 根据 "zoom level" 已经下采样的数据或原始数据。

我将该方法与 PyQtGraph 本机采用的默认自动下采样策略相结合,以进一步提高速度(可以通过@three_pineapples 建议进一步改进)。

这样,PyQtGraph 总是从低维数据开始,这使得缩放和平移即时,即使样本量非常大。

我的方法总结在这段代码中,猴子修补了PlotDataItem的getData方法。

# Downsample data
downsampled_data = downsample(data, 100)

# Replacement for the default getData function
def getData(obj):
    # Calculate the visible range
    range = obj.viewRect()
    if range is not None:
        dx = float(data[-1, 0] - data[0, 0]) / (data.size[0] - 1)
        x0 = (range.left() - data[0, 0]) / dx
        x1 = (range.right() - data[0, 0]) / dx
    # Decide whether to use downsampled or original data
    if (x1 - x0) > 20000:
        obj.xData = downsampled_data[:, 0]
        obj.yData = downsampled_data[:, 1]
    else:
        obj.xData = data[:, 0]
        obj.yData = data[:, 1]
    # Run the original getData of PlotDataItem
    return PlotDataItem.getData(obj)

# Replace the original getData with our getData
plot_data_item.getData = types.MethodType(getData, plot_data_item)