Commit c4019b90 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Overhaul of ScatterPlotItem to improve performance. (API should be mostly unchanged)

Much more efficient at rapid updates.
parent 50aa289a
......@@ -26,7 +26,7 @@ win.show()
p = ui.plot
data = np.random.normal(size=(50,500), scale=100)
sizeArray = np.random.random(500) * 20.
sizeArray = (np.random.random(500) * 20.).astype(int)
ptr = 0
lastTime = time()
fps = None
......@@ -49,7 +49,8 @@ def update():
s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps)
app.processEvents() ## force complete redraw for every plot
p.repaint()
#app.processEvents() ## force complete redraw for every plot
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
......
......@@ -25,6 +25,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
from .Qt import QtGui, QtCore
import numpy as np
import decimal, re
import ctypes
try:
import scipy.ndimage
......@@ -223,13 +224,15 @@ def mkColor(*args):
return QtGui.QColor(*args)
def mkBrush(*args):
def mkBrush(*args, **kwds):
"""
| Convenience function for constructing Brush.
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
| Calling mkBrush(None) returns an invisible brush.
"""
if len(args) == 1:
if 'color' in kwds:
color = kwds['color']
elif len(args) == 1:
arg = args[0]
if arg is None:
return QtGui.QBrush(QtCore.Qt.NoBrush)
......@@ -237,7 +240,7 @@ def mkBrush(*args):
return QtGui.QBrush(arg)
else:
color = arg
if len(args) > 1:
elif len(args) > 1:
color = args
return QtGui.QBrush(mkColor(color))
......@@ -579,7 +582,10 @@ def solveBilinearTransform(points1, points2):
def makeRGBA(*args, **kwds):
"""Equivalent to makeARGB(..., useRGBA=True)"""
kwds['useRGBA'] = True
return makeARGB(*args, **kwds)
def makeARGB(data, lut=None, levels=None, useRGBA=False):
"""
......@@ -605,7 +611,7 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
Lookup tables can be built using GradientWidget.
levels - List [min, max]; optionally rescale data before converting through the
lookup table. rescaled = (data-min) * len(lut) / (max-min)
useRGBA - If True, the data is returned in RGBA order. The default is
useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is
False, which returns in BGRA order for use with QImage.
"""
......@@ -779,30 +785,107 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
return imgData, alpha
def makeQImage(imgData, alpha):
"""Turn an ARGB array into QImage"""
def makeQImage(imgData, alpha=None, copy=True, transpose=True):
"""
Turn an ARGB array into QImage.
By default, the data is copied; changes to the array will not
be reflected in the image. The image will be given a 'data' attribute
pointing to the array which shares its data to prevent python
freeing that memory while the image is in use.
=========== ===================================================================
Arguments:
imgData Array of data to convert. Must have shape (width, height, 3 or 4)
and dtype=ubyte. The order of values in the 3rd axis must be
(b, g, r, a).
alpha If True, the QImage returned will have format ARGB32. If False,
the format will be RGB32. By default, _alpha_ is True if
array.shape[2] == 4.
copy If True, the data is copied before converting to QImage.
If False, the new QImage points directly to the data in the array.
Note that the array must be contiguous for this to work.
transpose If True (the default), the array x/y axes are transposed before
creating the image. Note that Qt expects the axes to be in
(height, width) order whereas pyqtgraph usually prefers the
opposite.
=========== ===================================================================
"""
## create QImage from buffer
prof = debug.Profiler('functions.makeQImage', disabled=True)
## If we didn't explicitly specify alpha, check the array shape.
if alpha is None:
alpha = (imgData.shape[2] == 4)
copied = False
if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp)
if copy is True:
d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype)
d2[:,:,:3] = imgData
d2[:,:,3] = 255
imgData = d2
copied = True
else:
raise Exception('Array has only 3 channels; cannot make QImage without copying.')
if alpha:
imgFormat = QtGui.QImage.Format_ARGB32
else:
imgFormat = QtGui.QImage.Format_RGB32
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
try:
buf = imgData.data
except AttributeError: ## happens when image data is non-contiguous
if transpose:
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
if not imgData.flags['C_CONTIGUOUS']:
if copy is False:
extra = ' (try setting transpose=False)' if transpose else ''
raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra)
imgData = np.ascontiguousarray(imgData)
buf = imgData.data
copied = True
prof.mark('1')
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
prof.mark('2')
qimage.data = imgData
prof.finish()
return qimage
if copy is True and copied is False:
imgData = imgData.copy()
addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData
return img
#try:
#buf = imgData.data
#except AttributeError: ## happens when image data is non-contiguous
#buf = imgData.data
#prof.mark('1')
#qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
#prof.mark('2')
#qimage.data = imgData
#prof.finish()
#return qimage
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
ptr = img.bits()
ptr.setsize(img.byteCount())
fmt = img.format()
if fmt == img.Format_RGB32:
arr = np.asarray(ptr).reshape(img.height(), img.width(), 3)
elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied:
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4)
if copy:
arr = arr.copy()
if transpose:
return arr.transpose((1,0,2))
else:
return arr
def rescaleData(data, scale, offset):
newData = np.empty((data.size,), dtype=np.int)
......
......@@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject):
'symbolBrush': (50, 50, 150),
'pxMode': True,
'pointMode': None,
'data': None,
}
self.setData(*args, **kargs)
......@@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject):
return QtCore.QRectF() ## let child items handle this
def setAlpha(self, alpha, auto):
if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto:
return
self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto
self.setOpacity(alpha)
#self.update()
def setFftMode(self, mode):
if self.opts['fftMode'] == mode:
return
self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None
self.updateItems()
def setLogMode(self, xMode, yMode):
self.opts['logMode'] = (xMode, yMode)
if self.opts['logMode'] == [xMode, yMode]:
return
self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None
self.updateItems()
def setPointMode(self, mode):
if self.opts['pointMode'] == mode:
return
self.opts['pointMode'] = mode
self.update()
......@@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject):
def setFillBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['fillBrush'] == brush:
return
self.opts['fillBrush'] = brush
self.updateItems()
......@@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject):
return self.setFillBrush(*args, **kargs)
def setFillLevel(self, level):
if self.opts['fillLevel'] == level:
return
self.opts['fillLevel'] = level
self.updateItems()
def setSymbol(self, symbol):
if self.opts['symbol'] == symbol:
return
self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol)
self.updateItems()
def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs)
if self.opts['symbolPen'] == pen:
return
self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen)
self.updateItems()
......@@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject):
def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush:
return
self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush)
self.updateItems()
def setSymbolSize(self, size):
if self.opts['symbolSize'] == size:
return
self.opts['symbolSize'] = size
#self.scatter.setSymbolSize(symbolSize)
self.updateItems()
def setDownsampling(self, ds):
if self.opts['downsample'] != ds:
self.opts['downsample'] = ds
self.xDisp = self.yDisp = None
self.updateItems()
if self.opts['downsample'] == ds:
return
self.opts['downsample'] = ds
self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs):
"""
......@@ -436,9 +459,12 @@ class PlotDataItem(GraphicsObject):
and max)
=============== =============================================================
"""
if frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
(x, y) = self.getData()
if x is None or len(x) == 0:
return (0, 0)
return None
if ax == 0:
d = x
......@@ -450,14 +476,15 @@ class PlotDataItem(GraphicsObject):
if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
d2 = d2[mask]
#d2 = d2[mask]
if frac >= 1.0:
return (np.min(d), np.max(d))
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
if len(d) > 0:
if frac >= 1.0:
return (np.min(d), np.max(d))
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
return None
def clear(self):
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment