From 63bf2b32701b2887533f1051f7e81d95b3b77c51 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 20 Sep 2013 15:46:10 +0800 Subject: [PATCH 1/9] optimize ScatterPlotItem with pxMode=True --- pyqtgraph/__init__.py | 4 +- pyqtgraph/functions.py | 40 ++++++++------ pyqtgraph/graphicsItems/AxisItem.py | 7 ++- pyqtgraph/graphicsItems/ScatterPlotItem.py | 62 ++++++++++++++-------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 12a4f90f..c6b411a1 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -48,8 +48,8 @@ else: CONFIG_OPTIONS = { '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 - 'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc. - 'background': (0, 0, 0), ## default background for GraphicsWidget + 'foreground': 'd', ## default foreground color for axes, labels, etc. + 'background': 'k', ## default background for GraphicsWidget 'antialias': False, 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'useWeave': True, ## Use weave to speed up some operations, if it is available diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 14e4e076..33069d23 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation. from __future__ import division from .python2_3 import asUnicode +from .Qt import QtGui, QtCore, USE_PYSIDE Colors = { - 'b': (0,0,255,255), - 'g': (0,255,0,255), - 'r': (255,0,0,255), - 'c': (0,255,255,255), - 'm': (255,0,255,255), - 'y': (255,255,0,255), - 'k': (0,0,0,255), - 'w': (255,255,255,255), + 'b': QtGui.QColor(0,0,255,255), + 'g': QtGui.QColor(0,255,0,255), + 'r': QtGui.QColor(255,0,0,255), + 'c': QtGui.QColor(0,255,255,255), + 'm': QtGui.QColor(255,0,255,255), + 'y': QtGui.QColor(255,255,0,255), + 'k': QtGui.QColor(0,0,0,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') @@ -23,7 +27,6 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' -from .Qt import QtGui, QtCore, USE_PYSIDE import pyqtgraph as pg import numpy as np import decimal, re @@ -169,17 +172,15 @@ def mkColor(*args): """ err = 'Not sure how to make a color from "%s"' % str(args) if len(args) == 1: - if 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 isinstance(args[0], basestring): + if isinstance(args[0], basestring): c = args[0] if c[0] == '#': c = 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: r = int(c[0]*2, 16) g = int(c[1]*2, 16) @@ -200,6 +201,11 @@ def mkColor(*args): g = int(c[2:4], 16) b = int(c[4:6], 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__'): if len(args[0]) == 3: (r, g, b) = args[0] @@ -283,7 +289,7 @@ def mkPen(*args, **kargs): color = args if color is None: - color = mkColor(200, 200, 200) + color = mkColor('l') if hsv is not None: color = hsvColor(*hsv) else: diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index d82f5d41..3f2f6fcd 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -266,8 +266,7 @@ class AxisItem(GraphicsWidget): self.picture = None def pen(self): - if self._pen is None: - return fn.mkPen(pg.getConfigOption('foreground')) + #return self._pen return pg.mkPen(self._pen) def setPen(self, pen): @@ -276,11 +275,11 @@ class AxisItem(GraphicsWidget): if pen == None, the default will be used (see :func:`setConfigOption `) """ - self._pen = pen self.picture = None if pen is None: pen = pg.getConfigOption('foreground') - self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6] + self._pen = pg.mkPen(pen) + self.labelStyle['color'] = '#' + pg.colorStr(self._pen.color())[:6] self.setLabel() self.update() diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 97f5aa8f..3cab5ffe 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -98,31 +98,41 @@ class SymbolAtlas(object): # weak value; if all external refs to this list disappear, # the symbol will be forgotten. self.symbolMap = weakref.WeakValueDictionary() + self.symbolPen = weakref.WeakValueDictionary() + self.symbolBrush = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap self.atlasValid = False + self.max_width=0 def getSymbolCoords(self, opts): """ 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) + keyi = None + coordi = None for i, rec in enumerate(opts): - symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] - pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen - brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush - key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color())) - if key not in self.symbolMap: - newCoords = SymbolAtlas.SymbolCoords() - self.symbolMap[key] = newCoords - self.atlasValid = False - #try: - #self.addToAtlas(key) ## squeeze this into the atlas if there is room - #except: - #self.buildAtlas() ## otherwise, we need to rebuild - - coords[i] = self.symbolMap[key] + key = (rec[3], rec[2], id(rec[4]), id(rec[5])) + if key == keyi: + coords[i]=coordi + else: + try: + coords[i] = self.symbolMap[key] + except KeyError: + newCoords = SymbolAtlas.SymbolCoords() + self.symbolMap[key] = newCoords + self.symbolPen[key] = rec['pen'] + self.symbolBrush[key] = rec['brush'] + self.atlasValid = False + #try: + #self.addToAtlas(key) ## squeeze this into the atlas if there is room + #except: + #self.buildAtlas() ## otherwise, we need to rebuild + coords[i] = newCoords + keyi = key + coordi = newCoords return coords def buildAtlas(self): @@ -133,8 +143,8 @@ class SymbolAtlas(object): images = [] for key, coords in self.symbolMap.items(): if len(coords) == 0: - pen = fn.mkPen(color=key[2], width=key[3], style=key[4]) - brush = fn.mkBrush(color=key[5]) + pen = self.symbolPen[key] + brush = self.symbolBrush[key] img = renderSymbol(key[0], key[1], pen, brush) images.append(img) ## we only need this to prevent the images being garbage collected immediately arr = fn.imageToArray(img, copy=False, transpose=False) @@ -181,6 +191,7 @@ class SymbolAtlas(object): self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True + self.max_width=maxWidth def getAtlas(self): if not self.atlasValid: @@ -237,8 +248,8 @@ class ScatterPlotItem(GraphicsObject): 'antialias': pg.getConfigOption('antialias'), } - self.setPen(200,200,200, update=False) - self.setBrush(100,100,150, update=False) + self.setPen('l', update=False) + self.setBrush('s', update=False) self.setSymbol('o', update=False) self.setSize(7, update=False) prof.mark('1') @@ -533,10 +544,8 @@ class ScatterPlotItem(GraphicsObject): def updateSpots(self, dataSet=None): if dataSet is None: dataSet = self.data - self._maxSpotWidth = 0 - self._maxSpotPxWidth = 0 + invalidate = False - self.measureSpotSizes(dataSet) if self.opts['pxMode']: mask = np.equal(dataSet['fragCoords'], None) if np.any(mask): @@ -549,6 +558,13 @@ class ScatterPlotItem(GraphicsObject): #if rec['fragCoords'] is None: #invalidate = True #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) + self.fragmentAtlas.getAtlas() + self._maxSpotPxWidth=self.fragmentAtlas.max_width + else: + self._maxSpotWidth = 0 + self._maxSpotPxWidth = 0 + self.measureSpotSizes(dataSet) + if invalidate: self.invalidate() @@ -666,7 +682,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - + def generateFragments(self): tr = self.deviceTransform() if tr is None: @@ -711,7 +727,7 @@ class ScatterPlotItem(GraphicsObject): #self.lastAtlas = arr if self.fragments is None: - self.updateSpots() + #self.updateSpots() self.generateFragments() p.resetTransform() From 3a9258e35edd8032e4d2dd41cd128cde2658ffcf Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 20 Sep 2013 16:46:33 +0800 Subject: [PATCH 2/9] Correct comment in examples/ScatterPlot.py --- examples/ScatterPlot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 805cf09f..c11c782a 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -27,7 +27,6 @@ w2.setAspectLocked(True) view.nextRow() w3 = 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: @@ -58,8 +57,9 @@ s1.sigClicked.connect(clicked) ## 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 -## and memory usage since each spot generates its own pre-rendered image. +## In this case, drawing is almsot as fast as 1), but there is more startup +## overhead and memory usage since each spot generates its own pre-rendered +## image. s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) From 26b84693a87163264c4d4fee36842ce1eeb68caf Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Sun, 22 Sep 2013 23:10:18 +0800 Subject: [PATCH 3/9] Modify for loop into map in ScatterPlotItem.py --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 3cab5ffe..64779b1f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -3,6 +3,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject +from itertools import starmap import numpy as np import scipy.stats import weakref @@ -149,7 +150,7 @@ class SymbolAtlas(object): images.append(img) ## we only need this to prevent the images being garbage collected immediately arr = fn.imageToArray(img, copy=False, transpose=False) else: - (x,y,w,h) = self.symbolMap[key] + (y,x,h,w) = self.symbolMap[key] arr = self.atlasData[x:x+w, y:y+w] rendered[key] = arr w = arr.shape[0] @@ -180,14 +181,14 @@ class SymbolAtlas(object): x = 0 rowheight = h self.atlasRows.append([y, rowheight, 0]) - self.symbolMap[key][:] = x, y, w, h + self.symbolMap[key][:] = y, x, h, w x += w self.atlasRows[-1][2] = x height = y + rowheight self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) for key in symbols: - x, y, w, h = self.symbolMap[key] + y, x, h, w = self.symbolMap[key] self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -694,12 +695,15 @@ class ScatterPlotItem(GraphicsObject): 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)) + #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)) + rect = starmap(QtCore.QRectF, self.data['fragCoords']) + pos = map(QtCore.QPointF, pts[0,:], pts[1,:]) + self.fragments = map(QtGui.QPainter.PixmapFragment.create, pos, rect) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) From f5ee45ac28da20fa52d039a789ca3e402986d5ce Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 00:45:55 +0800 Subject: [PATCH 4/9] Improve ScatterPlotItem Slightly faster and more memory efficient, correct python 3 bug --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 64779b1f..76390ba9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -4,6 +4,10 @@ import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject from itertools import starmap +try: + from itertools import imap +except ImportError: + imap = map import numpy as np import scipy.stats import weakref @@ -702,8 +706,8 @@ class ScatterPlotItem(GraphicsObject): # rect = QtCore.QRectF(y, x, h, w) # self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) rect = starmap(QtCore.QRectF, self.data['fragCoords']) - pos = map(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = map(QtGui.QPainter.PixmapFragment.create, pos, rect) + pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) + self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, rect)) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) From c3576b1c09f2204d64861e964a71e05a5cdaffb3 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 16:45:43 +0800 Subject: [PATCH 5/9] Some few more optimization to ScatterPlotItem --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 91 +++++++++++++--------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 76390ba9..8dcdcdf0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -92,9 +92,6 @@ class SymbolAtlas(object): pm = atlas.getAtlas() """ - class SymbolCoords(list): ## needed because lists are not allowed in weak references. - pass - def __init__(self): # symbol key : [x, y, w, h] atlas coordinates # note that the coordinate list will always be the same list object as @@ -102,9 +99,10 @@ class SymbolAtlas(object): # change if the atlas is rebuilt. # weak value; if all external refs to this list disappear, # the symbol will be forgotten. - self.symbolMap = weakref.WeakValueDictionary() self.symbolPen = weakref.WeakValueDictionary() self.symbolBrush = weakref.WeakValueDictionary() + self.symbolRectSrc = weakref.WeakValueDictionary() + self.symbolRectTarg = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap @@ -115,30 +113,34 @@ class SymbolAtlas(object): """ 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) + rectTarg = np.empty(len(opts), dtype=object) keyi = None - coordi = None + rectSrci = None + rectTargi = None for i, rec in enumerate(opts): key = (rec[3], rec[2], id(rec[4]), id(rec[5])) if key == keyi: - coords[i]=coordi + rectSrc[i] = rectSrci + rectTarg[i] = rectTargi else: try: - coords[i] = self.symbolMap[key] + rectSrc[i] = self.symbolRectSrc[key] + rectTarg[i] = self.symbolRectTarg[key] except KeyError: - newCoords = SymbolAtlas.SymbolCoords() - self.symbolMap[key] = newCoords + newRectSrc = QtCore.QRectF() + newRectTarg = QtCore.QRectF() self.symbolPen[key] = rec['pen'] self.symbolBrush[key] = rec['brush'] + self.symbolRectSrc[key] = newRectSrc + self.symbolRectTarg[key] = newRectTarg self.atlasValid = False - #try: - #self.addToAtlas(key) ## squeeze this into the atlas if there is room - #except: - #self.buildAtlas() ## otherwise, we need to rebuild - coords[i] = newCoords + rectSrc[i] = self.symbolRectSrc[key] + rectTarg[i] = self.symbolRectTarg[key] keyi = key - coordi = newCoords - return coords + rectSrci = self.symbolRectSrc[key] + rectTargi = self.symbolRectTarg[key] + return rectSrc, rectTarg def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -146,15 +148,15 @@ class SymbolAtlas(object): avgWidth = 0.0 maxWidth = 0 images = [] - for key, coords in self.symbolMap.items(): - if len(coords) == 0: + for key, rectSrc in self.symbolRectSrc.items(): + if rectSrc.width() == 0: pen = self.symbolPen[key] brush = self.symbolBrush[key] img = renderSymbol(key[0], key[1], pen, brush) images.append(img) ## we only need this to prevent the images being garbage collected immediately arr = fn.imageToArray(img, copy=False, transpose=False) else: - (y,x,h,w) = self.symbolMap[key] + (y,x,h,w) = rectSrc.getRect() arr = self.atlasData[x:x+w, y:y+w] rendered[key] = arr w = arr.shape[0] @@ -185,14 +187,15 @@ class SymbolAtlas(object): x = 0 rowheight = h self.atlasRows.append([y, rowheight, 0]) - self.symbolMap[key][:] = y, x, h, w + self.symbolRectSrc[key].setRect(y, x, h, w) x += w self.atlasRows[-1][2] = x height = y + rowheight self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) for key in symbols: - y, x, h, w = self.symbolMap[key] + y, x, h, w = self.symbolRectSrc[key].getRect() + self.symbolRectTarg[key].setRect(-h/2, -w/2, h, w) self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -241,9 +244,10 @@ class ScatterPlotItem(GraphicsObject): self.picture = None # QPicture used for rendering when pxmode==False self.fragments = None # fragment specification for pxmode; updated every time the view changes. + self.tar = None 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)]) self.bounds = [None, None] ## caches data bounds 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 @@ -412,6 +416,7 @@ class ScatterPlotItem(GraphicsObject): ## clear any cached drawing state self.picture = None self.fragments = None + self.tar = None self.update() def getData(self): @@ -446,7 +451,7 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['pen'] = fn.mkPen(*args, **kargs) - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -471,7 +476,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['brush'] = fn.mkBrush(*args, **kargs) #self._spotPixmap = None - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -494,7 +499,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['symbol'] = symbol self._spotPixmap = None - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -517,7 +522,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['size'] = size self._spotPixmap = None - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -552,12 +557,13 @@ class ScatterPlotItem(GraphicsObject): invalidate = False if self.opts['pxMode']: - mask = np.equal(dataSet['fragCoords'], None) + mask = np.equal(dataSet['rectSrc'], None) if np.any(mask): invalidate = True opts = self.getSpotOpts(dataSet[mask]) - coords = self.fragmentAtlas.getSymbolCoords(opts) - dataSet['fragCoords'][mask] = coords + rectSrc, rectTarg = self.fragmentAtlas.getSymbolCoords(opts) + dataSet['rectSrc'][mask] = rectSrc + dataSet['rectTarg'][mask] = rectTarg #for rec in dataSet: #if rec['fragCoords'] is None: @@ -687,6 +693,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None + self.tar = None def generateFragments(self): tr = self.deviceTransform() @@ -705,9 +712,8 @@ class ScatterPlotItem(GraphicsObject): # x,y,w,h = rec['fragCoords'] # rect = QtCore.QRectF(y, x, h, w) # self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) - rect = starmap(QtCore.QRectF, self.data['fragCoords']) pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, rect)) + self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, self.data['rectSrc'])) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) @@ -734,15 +740,28 @@ class ScatterPlotItem(GraphicsObject): #print "Atlas changed:", arr #self.lastAtlas = arr - if self.fragments is None: + #if self.fragments is None: #self.updateSpots() - self.generateFragments() + #self.generateFragments() p.resetTransform() if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: - p.drawPixmapFragments(self.fragments, atlas) + 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) + pts = np.clip(pts, -2**30, 2**30) + if self.tar == None: + self.tar = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) + p.drawPixmapFragments(self.tar, self.data['rectSrc'].tolist(), atlas) + #p.drawPixmapFragments(self.fragments, atlas) else: + if self.fragments is None: + self.generateFragments() p.setRenderHint(p.Antialiasing, aa) for i in range(len(self.data)): @@ -911,7 +930,7 @@ class SpotItem(object): self._data['data'] = data def updateItem(self): - self._data['fragCoords'] = None + self._data['rectSrc'] = None self._plot.updateSpots(self._data.reshape(1)) self._plot.invalidate() From bd43a7508afb60b29a0047504279d68a28817f20 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 17:47:33 +0800 Subject: [PATCH 6/9] Rename self.tar to self.target --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 8dcdcdf0..98643582 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -244,7 +244,7 @@ class ScatterPlotItem(GraphicsObject): self.picture = None # QPicture used for rendering when pxmode==False self.fragments = None # fragment specification for pxmode; updated every time the view changes. - self.tar = None + self.target = None self.fragmentAtlas = SymbolAtlas() 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)]) @@ -416,7 +416,7 @@ class ScatterPlotItem(GraphicsObject): ## clear any cached drawing state self.picture = None self.fragments = None - self.tar = None + self.target = None self.update() def getData(self): @@ -693,7 +693,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - self.tar = None + self.target = None def generateFragments(self): tr = self.deviceTransform() @@ -755,9 +755,9 @@ class ScatterPlotItem(GraphicsObject): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) pts = np.clip(pts, -2**30, 2**30) - if self.tar == None: - self.tar = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) - p.drawPixmapFragments(self.tar, self.data['rectSrc'].tolist(), atlas) + if self.target == None: + self.target = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) + p.drawPixmapFragments(self.target, self.data['rectSrc'].tolist(), atlas) #p.drawPixmapFragments(self.fragments, atlas) else: if self.fragments is None: From 73a079a64922e5744be1f3078ab0c304eb96c22b Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 16:12:29 +0800 Subject: [PATCH 7/9] Improve ScatterPlotItem.py Add optimization for PySide, Plot only visible symbole, cache rectTarg --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 67 ++++++++++++++-------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 98643582..71e94e57 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -3,7 +3,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject -from itertools import starmap +from itertools import starmap, repeat try: from itertools import imap except ImportError: @@ -102,7 +102,6 @@ class SymbolAtlas(object): self.symbolPen = weakref.WeakValueDictionary() self.symbolBrush = weakref.WeakValueDictionary() self.symbolRectSrc = weakref.WeakValueDictionary() - self.symbolRectTarg = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap @@ -114,33 +113,25 @@ class SymbolAtlas(object): Given a list of spot records, return an object representing the coordinates of that symbol within the atlas """ rectSrc = np.empty(len(opts), dtype=object) - rectTarg = np.empty(len(opts), dtype=object) keyi = None rectSrci = None - rectTargi = None for i, rec in enumerate(opts): key = (rec[3], rec[2], id(rec[4]), id(rec[5])) if key == keyi: rectSrc[i] = rectSrci - rectTarg[i] = rectTargi else: try: rectSrc[i] = self.symbolRectSrc[key] - rectTarg[i] = self.symbolRectTarg[key] except KeyError: newRectSrc = QtCore.QRectF() - newRectTarg = QtCore.QRectF() self.symbolPen[key] = rec['pen'] self.symbolBrush[key] = rec['brush'] self.symbolRectSrc[key] = newRectSrc - self.symbolRectTarg[key] = newRectTarg self.atlasValid = False rectSrc[i] = self.symbolRectSrc[key] - rectTarg[i] = self.symbolRectTarg[key] keyi = key rectSrci = self.symbolRectSrc[key] - rectTargi = self.symbolRectTarg[key] - return rectSrc, rectTarg + return rectSrc def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -195,7 +186,6 @@ class SymbolAtlas(object): self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) for key in symbols: y, x, h, w = self.symbolRectSrc[key].getRect() - self.symbolRectTarg[key].setRect(-h/2, -w/2, h, w) self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -247,7 +237,7 @@ class ScatterPlotItem(GraphicsObject): self.target = None self.fragmentAtlas = SymbolAtlas() - 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)]) + 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._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 @@ -561,15 +551,17 @@ class ScatterPlotItem(GraphicsObject): if np.any(mask): invalidate = True opts = self.getSpotOpts(dataSet[mask]) - rectSrc, rectTarg = self.fragmentAtlas.getSymbolCoords(opts) + rectSrc = self.fragmentAtlas.getSymbolCoords(opts) dataSet['rectSrc'][mask] = rectSrc - dataSet['rectTarg'][mask] = rectTarg + #for rec in dataSet: #if rec['fragCoords'] is None: #invalidate = True #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 @@ -699,9 +691,15 @@ class ScatterPlotItem(GraphicsObject): 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'] + mask = np.logical_and( + np.logical_and(self.data['x'] - self.data['width'] > range[0][0], + self.data['x'] + self.data['width'] < range[0][1]), + np.logical_and(self.data['y'] - self.data['width'] > range[1][0], + self.data['y'] + self.data['width'] < range[1][1])) ## remove out of view points + data = self.data[mask] + pts = np.empty((2,len(data['x']))) + pts[0] = data['x'] + pts[1] = data['y'] pts = fn.transformCoordinates(tr, pts) self.fragments = [] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. @@ -746,18 +744,39 @@ class ScatterPlotItem(GraphicsObject): p.resetTransform() - if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: + if self.opts['useCache'] and self._exportOpts is False: 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'] + 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) if self.target == None: - self.target = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) - p.drawPixmapFragments(self.target, self.data['rectSrc'].tolist(), atlas) + 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) #p.drawPixmapFragments(self.fragments, atlas) else: if self.fragments is None: From 36979b67ea3b99bbb5b7d08e6a9eb4fd0a3d0246 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 17:06:51 +0800 Subject: [PATCH 8/9] Clean ScatterPlotItem --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 103 ++++++++------------- 1 file changed, 37 insertions(+), 66 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 71e94e57..2d76b104 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -687,35 +687,40 @@ class ScatterPlotItem(GraphicsObject): self.fragments = None self.target = None - def generateFragments(self): + def setExportMode(self, *args, **kwds): + GraphicsObject.setExportMode(self, *args, **kwds) + self.invalidate() + + + def getTransformedPoint(self): tr = self.deviceTransform() if tr is None: - return - mask = np.logical_and( - np.logical_and(self.data['x'] - self.data['width'] > range[0][0], - self.data['x'] + self.data['width'] < range[0][1]), - np.logical_and(self.data['y'] - self.data['width'] > range[1][0], - self.data['y'] + self.data['width'] < range[1][1])) ## remove out of view points - data = self.data[mask] + 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) - 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)) - pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, self.data['rectSrc'])) - - def setExportMode(self, *args, **kwds): - GraphicsObject.setExportMode(self, *args, **kwds) - self.invalidate() + pts -= data['width'] + pts = np.clip(pts, -2**30, 2**30) + return data, pts @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): @@ -732,44 +737,14 @@ class ScatterPlotItem(GraphicsObject): if self.opts['pxMode'] is True: 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() + data, pts = self.getTransformedPoint() + if data is None: + return + if self.opts['useCache'] and self._exportOpts is False: - tr = self.deviceTransform() - if tr is None: - return - 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) + if self.target == None: list(imap(QtCore.QRectF.moveTo, data['rectTarg'], pts[0,:], pts[1,:])) self.target=data['rectTarg'] @@ -777,17 +752,13 @@ class ScatterPlotItem(GraphicsObject): list(imap(p.drawPixmap, self.target, repeat(atlas), data['rectSrc'])) else: p.drawPixmapFragments(self.target.tolist(), data['rectSrc'].tolist(), atlas) - #p.drawPixmapFragments(self.fragments, atlas) else: - if self.fragments is None: - self.generateFragments() p.setRenderHint(p.Antialiasing, aa) - + for i in range(len(self.data)): - rec = self.data[i] - frag = self.fragments[i] + rec = data[i] p.resetTransform() - p.translate(frag.x, frag.y) + p.translate(pts[0,i], pts[1,i]) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: @@ -798,7 +769,7 @@ class ScatterPlotItem(GraphicsObject): rec = rec.copy() rec['size'] *= scale p2.resetTransform() - p2.translate(rec['x'], rec['y']) + p2.translate(rec['x']+rec['width'], rec['y']+rec['width']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() From d22403f84f9c53547d74a3bfdd74973209a2e91e Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 17:13:13 +0800 Subject: [PATCH 9/9] Correct symbols position --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 2d76b104..1eedf0e7 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -758,7 +758,7 @@ class ScatterPlotItem(GraphicsObject): for i in range(len(self.data)): rec = data[i] p.resetTransform() - p.translate(pts[0,i], pts[1,i]) + p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: @@ -769,7 +769,7 @@ class ScatterPlotItem(GraphicsObject): rec = rec.copy() rec['size'] *= scale p2.resetTransform() - p2.translate(rec['x']+rec['width'], rec['y']+rec['width']) + p2.translate(rec['x'], rec['y']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end()