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

时间:2015-05-28 05:35:50

标签: python plot pyqtgraph

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

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

我怎样才能做到这一点?

2 个答案:

答案 0 :(得分:1)

我在一个名为runviewer的项目中做过类似的事情。一般的想法是每当绘图的x范围改变时重新采样数据。我们使用的近似方法是:

  • 将方法连接到sigXRangeChanged的{​​{1}}信号,该信号设置一个布尔标志,指示需要重新采样的数据。

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

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

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

你也可以通过不轮询标志,但使用某种线程事件/条件来改善这一点。

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

有关效果的其他评论

我怀疑pyqtgraph内置方法的部分性能问题是下采样是在主线程中完成的。因此,必须在图形再次响应用户输入之前完成下采样。我们的方法避免了这一点。我们的方法还将下采样的次数限制为最多每QThread秒一次。因此,在我们使用延迟的情况下,我们每隔0.5-1秒仅进行一次下采样,同时保持主线程(以及UI)的响应。它确实意味着如果用户快速放大,用户可能会看到粗略采样的数据,但这在最多2次重采样迭代中得到纠正(因此最多延迟1-2秒)。此外,由于校正需要很短的时间,因此使用新采样数据进行更新/重绘通常在用户完成与UI的交互后完成,因此在重绘期间不会发现任何无响应。

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

答案 1 :(得分:0)

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

因此,我决定实施一种不同的策略,即预先对数据进行采样,然后根据“缩放级别”选择已经下采样的数据或原始数据。

我将这种方法与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)