Merge remote-tracking branch 'gpoulin/scatter_optim' into scatter-optim

Conflicts:
	pyqtgraph/functions.py
	pyqtgraph/graphicsItems/AxisItem.py
This commit is contained in:
Luke Campagnola 2014-01-15 00:11:05 -05:00
commit 704f2f2048
5 changed files with 139 additions and 99 deletions

View File

@ -27,7 +27,6 @@ w2.setAspectLocked(True)
view.nextRow() view.nextRow()
w3 = view.addPlot() w3 = view.addPlot()
w4 = view.addPlot() w4 = view.addPlot()
print("Generating data, this takes a few seconds...")
## There are a few different ways we can draw scatter plots; each is optimized for different types of data: ## There are a few different ways we can draw scatter plots; each is optimized for different types of data:
@ -58,8 +57,9 @@ s1.sigClicked.connect(clicked)
## 2) Spots are transform-invariant, but not identical (top-right plot). ## 2) Spots are transform-invariant, but not identical (top-right plot).
## In this case, drawing is as fast as 1), but there is more startup overhead ## In this case, drawing is almsot as fast as 1), but there is more startup
## and memory usage since each spot generates its own pre-rendered image. ## overhead and memory usage since each spot generates its own pre-rendered
## image.
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
pos = np.random.normal(size=(2,n), scale=1e-5) pos = np.random.normal(size=(2,n), scale=1e-5)

View File

@ -48,8 +48,8 @@ else:
CONFIG_OPTIONS = { CONFIG_OPTIONS = {
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc. 'foreground': 'd', ## default foreground color for axes, labels, etc.
'background': (0, 0, 0), ## default background for GraphicsWidget 'background': 'k', ## default background for GraphicsWidget
'antialias': False, 'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available 'useWeave': True, ## Use weave to speed up some operations, if it is available

View File

@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
from __future__ import division from __future__ import division
from .python2_3 import asUnicode from .python2_3 import asUnicode
from .Qt import QtGui, QtCore, USE_PYSIDE
Colors = { Colors = {
'b': (0,0,255,255), 'b': QtGui.QColor(0,0,255,255),
'g': (0,255,0,255), 'g': QtGui.QColor(0,255,0,255),
'r': (255,0,0,255), 'r': QtGui.QColor(255,0,0,255),
'c': (0,255,255,255), 'c': QtGui.QColor(0,255,255,255),
'm': (255,0,255,255), 'm': QtGui.QColor(255,0,255,255),
'y': (255,255,0,255), 'y': QtGui.QColor(255,255,0,255),
'k': (0,0,0,255), 'k': QtGui.QColor(0,0,0,255),
'w': (255,255,255,255), 'w': QtGui.QColor(255,255,255,255),
'd': QtGui.QColor(150,150,150,255),
'l': QtGui.QColor(200,200,200,255),
's': QtGui.QColor(100,100,150,255),
} }
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
@ -168,17 +172,15 @@ def mkColor(*args):
""" """
err = 'Not sure how to make a color from "%s"' % str(args) err = 'Not sure how to make a color from "%s"' % str(args)
if len(args) == 1: if len(args) == 1:
if isinstance(args[0], QtGui.QColor): if isinstance(args[0], basestring):
return QtGui.QColor(args[0])
elif isinstance(args[0], float):
r = g = b = int(args[0] * 255)
a = 255
elif isinstance(args[0], basestring):
c = args[0] c = args[0]
if c[0] == '#': if c[0] == '#':
c = c[1:] c = c[1:]
if len(c) == 1: if len(c) == 1:
(r, g, b, a) = Colors[c] try:
return Colors[c]
except KeyError:
raise Exception(err)
if len(c) == 3: if len(c) == 3:
r = int(c[0]*2, 16) r = int(c[0]*2, 16)
g = int(c[1]*2, 16) g = int(c[1]*2, 16)
@ -199,6 +201,11 @@ def mkColor(*args):
g = int(c[2:4], 16) g = int(c[2:4], 16)
b = int(c[4:6], 16) b = int(c[4:6], 16)
a = int(c[6:8], 16) a = int(c[6:8], 16)
elif isinstance(args[0], QtGui.QColor):
return QtGui.QColor(args[0])
elif isinstance(args[0], float):
r = g = b = int(args[0] * 255)
a = 255
elif hasattr(args[0], '__len__'): elif hasattr(args[0], '__len__'):
if len(args[0]) == 3: if len(args[0]) == 3:
(r, g, b) = args[0] (r, g, b) = args[0]
@ -282,7 +289,7 @@ def mkPen(*args, **kargs):
color = args color = args
if color is None: if color is None:
color = mkColor(200, 200, 200) color = mkColor('l')
if hsv is not None: if hsv is not None:
color = hsvColor(*hsv) color = hsvColor(*hsv)
else: else:

View File

@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget):
if pen == None, the default will be used (see :func:`setConfigOption if pen == None, the default will be used (see :func:`setConfigOption
<pyqtgraph.setConfigOption>`) <pyqtgraph.setConfigOption>`)
""" """
self._pen = pen
self.picture = None self.picture = None
if pen is None: if pen is None:
pen = getConfigOption('foreground') pen = getConfigOption('foreground')
self.labelStyle['color'] = '#' + fn.colorStr(fn.mkPen(pen).color())[:6] self._pen = fn.mkPen(pen)
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
self.setLabel() self.setLabel()
self.update() self.update()

View File

@ -3,6 +3,11 @@ from ..Point import Point
from .. import functions as fn from .. import functions as fn
from .GraphicsItem import GraphicsItem from .GraphicsItem import GraphicsItem
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from itertools import starmap, repeat
try:
from itertools import imap
except ImportError:
imap = map
import numpy as np import numpy as np
import weakref import weakref
from .. import getConfigOption from .. import getConfigOption
@ -86,9 +91,6 @@ class SymbolAtlas(object):
pm = atlas.getAtlas() pm = atlas.getAtlas()
""" """
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
pass
def __init__(self): def __init__(self):
# symbol key : [x, y, w, h] atlas coordinates # symbol key : [x, y, w, h] atlas coordinates
# note that the coordinate list will always be the same list object as # note that the coordinate list will always be the same list object as
@ -96,33 +98,39 @@ class SymbolAtlas(object):
# change if the atlas is rebuilt. # change if the atlas is rebuilt.
# weak value; if all external refs to this list disappear, # weak value; if all external refs to this list disappear,
# the symbol will be forgotten. # the symbol will be forgotten.
self.symbolMap = weakref.WeakValueDictionary() self.symbolPen = weakref.WeakValueDictionary()
self.symbolBrush = weakref.WeakValueDictionary()
self.symbolRectSrc = weakref.WeakValueDictionary()
self.atlasData = None # numpy array of atlas image self.atlasData = None # numpy array of atlas image
self.atlas = None # atlas as QPixmap self.atlas = None # atlas as QPixmap
self.atlasValid = False self.atlasValid = False
self.max_width=0
def getSymbolCoords(self, opts): def getSymbolCoords(self, opts):
""" """
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
""" """
coords = np.empty(len(opts), dtype=object) rectSrc = np.empty(len(opts), dtype=object)
keyi = None
rectSrci = None
for i, rec in enumerate(opts): for i, rec in enumerate(opts):
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] key = (rec[3], rec[2], id(rec[4]), id(rec[5]))
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen if key == keyi:
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush rectSrc[i] = rectSrci
key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color())) else:
if key not in self.symbolMap: try:
newCoords = SymbolAtlas.SymbolCoords() rectSrc[i] = self.symbolRectSrc[key]
self.symbolMap[key] = newCoords except KeyError:
self.atlasValid = False newRectSrc = QtCore.QRectF()
#try: self.symbolPen[key] = rec['pen']
#self.addToAtlas(key) ## squeeze this into the atlas if there is room self.symbolBrush[key] = rec['brush']
#except: self.symbolRectSrc[key] = newRectSrc
#self.buildAtlas() ## otherwise, we need to rebuild self.atlasValid = False
rectSrc[i] = self.symbolRectSrc[key]
coords[i] = self.symbolMap[key] keyi = key
return coords rectSrci = self.symbolRectSrc[key]
return rectSrc
def buildAtlas(self): def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width # get rendered array for all symbols, keep track of avg/max width
@ -130,15 +138,15 @@ class SymbolAtlas(object):
avgWidth = 0.0 avgWidth = 0.0
maxWidth = 0 maxWidth = 0
images = [] images = []
for key, coords in self.symbolMap.items(): for key, rectSrc in self.symbolRectSrc.items():
if len(coords) == 0: if rectSrc.width() == 0:
pen = fn.mkPen(color=key[2], width=key[3], style=key[4]) pen = self.symbolPen[key]
brush = fn.mkBrush(color=key[5]) brush = self.symbolBrush[key]
img = renderSymbol(key[0], key[1], pen, brush) img = renderSymbol(key[0], key[1], pen, brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False) arr = fn.imageToArray(img, copy=False, transpose=False)
else: else:
(x,y,w,h) = self.symbolMap[key] (y,x,h,w) = rectSrc.getRect()
arr = self.atlasData[x:x+w, y:y+w] arr = self.atlasData[x:x+w, y:y+w]
rendered[key] = arr rendered[key] = arr
w = arr.shape[0] w = arr.shape[0]
@ -169,17 +177,18 @@ class SymbolAtlas(object):
x = 0 x = 0
rowheight = h rowheight = h
self.atlasRows.append([y, rowheight, 0]) self.atlasRows.append([y, rowheight, 0])
self.symbolMap[key][:] = x, y, w, h self.symbolRectSrc[key].setRect(y, x, h, w)
x += w x += w
self.atlasRows[-1][2] = x self.atlasRows[-1][2] = x
height = y + rowheight height = y + rowheight
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
for key in symbols: for key in symbols:
x, y, w, h = self.symbolMap[key] y, x, h, w = self.symbolRectSrc[key].getRect()
self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlasData[x:x+w, y:y+h] = rendered[key]
self.atlas = None self.atlas = None
self.atlasValid = True self.atlasValid = True
self.max_width=maxWidth
def getAtlas(self): def getAtlas(self):
if not self.atlasValid: if not self.atlasValid:
@ -224,9 +233,10 @@ class ScatterPlotItem(GraphicsObject):
self.picture = None # QPicture used for rendering when pxmode==False self.picture = None # QPicture used for rendering when pxmode==False
self.fragments = None # fragment specification for pxmode; updated every time the view changes. self.fragments = None # fragment specification for pxmode; updated every time the view changes.
self.target = None
self.fragmentAtlas = SymbolAtlas() self.fragmentAtlas = SymbolAtlas()
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)]) self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('rectSrc', object), ('rectTarg', object), ('width', float)])
self.bounds = [None, None] ## caches data bounds self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
@ -237,8 +247,8 @@ class ScatterPlotItem(GraphicsObject):
'name': None, 'name': None,
} }
self.setPen(200,200,200, update=False) self.setPen('l', update=False)
self.setBrush(100,100,150, update=False) self.setBrush('s', update=False)
self.setSymbol('o', update=False) self.setSymbol('o', update=False)
self.setSize(7, update=False) self.setSize(7, update=False)
profiler() profiler()
@ -397,6 +407,7 @@ class ScatterPlotItem(GraphicsObject):
## clear any cached drawing state ## clear any cached drawing state
self.picture = None self.picture = None
self.fragments = None self.fragments = None
self.target = None
self.update() self.update()
def getData(self): def getData(self):
@ -434,7 +445,7 @@ class ScatterPlotItem(GraphicsObject):
else: else:
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
dataSet['fragCoords'] = None dataSet['rectSrc'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -459,7 +470,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
#self._spotPixmap = None #self._spotPixmap = None
dataSet['fragCoords'] = None dataSet['rectSrc'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -482,7 +493,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None dataSet['rectSrc'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -505,7 +516,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size self.opts['size'] = size
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None dataSet['rectSrc'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -537,22 +548,30 @@ class ScatterPlotItem(GraphicsObject):
def updateSpots(self, dataSet=None): def updateSpots(self, dataSet=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
invalidate = False invalidate = False
self.measureSpotSizes(dataSet)
if self.opts['pxMode']: if self.opts['pxMode']:
mask = np.equal(dataSet['fragCoords'], None) mask = np.equal(dataSet['rectSrc'], None)
if np.any(mask): if np.any(mask):
invalidate = True invalidate = True
opts = self.getSpotOpts(dataSet[mask]) opts = self.getSpotOpts(dataSet[mask])
coords = self.fragmentAtlas.getSymbolCoords(opts) rectSrc = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['fragCoords'][mask] = coords dataSet['rectSrc'][mask] = rectSrc
#for rec in dataSet: #for rec in dataSet:
#if rec['fragCoords'] is None: #if rec['fragCoords'] is None:
#invalidate = True #invalidate = True
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
self.fragmentAtlas.getAtlas()
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['rectSrc'])))/2
dataSet['rectTarg'] = list(imap(QtCore.QRectF, repeat(0), repeat(0), dataSet['width']*2, dataSet['width']*2))
self._maxSpotPxWidth=self.fragmentAtlas.max_width
else:
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
self.measureSpotSizes(dataSet)
if invalidate: if invalidate:
self.invalidate() self.invalidate()
@ -670,28 +689,42 @@ class ScatterPlotItem(GraphicsObject):
GraphicsObject.viewTransformChanged(self) GraphicsObject.viewTransformChanged(self)
self.bounds = [None, None] self.bounds = [None, None]
self.fragments = None self.fragments = None
self.target = None
def generateFragments(self):
tr = self.deviceTransform()
if tr is None:
return
pts = np.empty((2,len(self.data['x'])))
pts[0] = self.data['x']
pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts)
self.fragments = []
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
## Still won't be able to render correctly, though.
for i in xrange(len(self.data)):
rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i])
x,y,w,h = rec['fragCoords']
rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def setExportMode(self, *args, **kwds): def setExportMode(self, *args, **kwds):
GraphicsObject.setExportMode(self, *args, **kwds) GraphicsObject.setExportMode(self, *args, **kwds)
self.invalidate() self.invalidate()
def getTransformedPoint(self):
tr = self.deviceTransform()
if tr is None:
return None, None
## Remove out of view points
w = np.empty((2,len(self.data['width'])))
w[0] = self.data['width']
w[1] = self.data['width']
q, intv = tr.inverted()
if intv:
w = fn.transformCoordinates(q, w)
w=np.abs(w)
range = self.getViewBox().viewRange()
mask = np.logical_and(
np.logical_and(self.data['x'] + w[0,:] > range[0][0],
self.data['x'] - w[0,:] < range[0][1]),
np.logical_and(self.data['y'] + w[0,:] > range[1][0],
self.data['y'] - w[0,:] < range[1][1])) ## remove out of view points
data = self.data[mask]
else:
data = self.data
pts = np.empty((2,len(data['x'])))
pts[0] = data['x']
pts[1] = data['y']
pts = fn.transformCoordinates(tr, pts)
pts -= data['width']
pts = np.clip(pts, -2**30, 2**30)
return data, pts
@debug.warnOnException ## raising an exception here causes crash @debug.warnOnException ## raising an exception here causes crash
def paint(self, p, *args): def paint(self, p, *args):
@ -708,28 +741,28 @@ class ScatterPlotItem(GraphicsObject):
if self.opts['pxMode'] is True: if self.opts['pxMode'] is True:
atlas = self.fragmentAtlas.getAtlas() atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True)
#if hasattr(self, 'lastAtlas'):
#if np.any(self.lastAtlas != arr):
#print "Atlas changed:", arr
#self.lastAtlas = arr
if self.fragments is None:
self.updateSpots()
self.generateFragments()
p.resetTransform() p.resetTransform()
if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: data, pts = self.getTransformedPoint()
p.drawPixmapFragments(self.fragments, atlas) if data is None:
return
if self.opts['useCache'] and self._exportOpts is False:
if self.target == None:
list(imap(QtCore.QRectF.moveTo, data['rectTarg'], pts[0,:], pts[1,:]))
self.target=data['rectTarg']
if USE_PYSIDE:
list(imap(p.drawPixmap, self.target, repeat(atlas), data['rectSrc']))
else:
p.drawPixmapFragments(self.target.tolist(), data['rectSrc'].tolist(), atlas)
else: else:
p.setRenderHint(p.Antialiasing, aa) p.setRenderHint(p.Antialiasing, aa)
for i in range(len(self.data)): for i in range(len(self.data)):
rec = self.data[i] rec = data[i]
frag = self.fragments[i]
p.resetTransform() p.resetTransform()
p.translate(frag.x, frag.y) p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width'])
drawSymbol(p, *self.getSpotOpts(rec, scale)) drawSymbol(p, *self.getSpotOpts(rec, scale))
else: else:
if self.picture is None: if self.picture is None:
@ -891,7 +924,7 @@ class SpotItem(object):
self._data['data'] = data self._data['data'] = data
def updateItem(self): def updateItem(self):
self._data['fragCoords'] = None self._data['rectSrc'] = None
self._plot.updateSpots(self._data.reshape(1)) self._plot.updateSpots(self._data.reshape(1))
self._plot.invalidate() self._plot.invalidate()