Features:
- Canvas: added per-item context menus - Isocurve: option to extend curves to array boundaries option to generate QPainterPath instead of vertex array - Isosurface is a bajillion times faster - ViewBox added clear() method added locate(item) method (shows where an item is for debugging) Bugfixes: - automated example testing working properly - Exporter gets incorrect source rect when operating on PlotWidget - Set correct DPI and size for SVG exporter - GLMeshItem works properly with whole-mesh color specified as sequence - bugfix in functions.transformCoordinates for rotated matrices - reload library checks for modules that are imported multiple times - GraphicsObject, UIGraphicsItem: added workaround for PyQt / itemChange bug - ScatterPlotItem: disable cached render during export Other: - added documentation for several functions - minor updates to setup.py
This commit is contained in:
commit
b25e34f564
@ -93,6 +93,15 @@ class Canvas(QtGui.QWidget):
|
|||||||
self.registeredName = CanvasManager.instance().registerCanvas(self, name)
|
self.registeredName = CanvasManager.instance().registerCanvas(self, name)
|
||||||
self.ui.redirectCombo.setHostName(self.registeredName)
|
self.ui.redirectCombo.setHostName(self.registeredName)
|
||||||
|
|
||||||
|
self.menu = QtGui.QMenu()
|
||||||
|
#self.menu.setTitle("Image")
|
||||||
|
remAct = QtGui.QAction("Remove item", self.menu)
|
||||||
|
remAct.triggered.connect(self.removeClicked)
|
||||||
|
self.menu.addAction(remAct)
|
||||||
|
self.menu.remAct = remAct
|
||||||
|
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
|
||||||
|
|
||||||
|
|
||||||
def storeSvg(self):
|
def storeSvg(self):
|
||||||
self.ui.view.writeSvg()
|
self.ui.view.writeSvg()
|
||||||
|
|
||||||
@ -513,10 +522,20 @@ class Canvas(QtGui.QWidget):
|
|||||||
listItem.setCheckState(0, QtCore.Qt.Unchecked)
|
listItem.setCheckState(0, QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
def removeItem(self, item):
|
def removeItem(self, item):
|
||||||
|
if isinstance(item, QtGui.QTreeWidgetItem):
|
||||||
|
item = item.canvasItem()
|
||||||
|
|
||||||
|
|
||||||
if isinstance(item, CanvasItem):
|
if isinstance(item, CanvasItem):
|
||||||
item.setCanvas(None)
|
item.setCanvas(None)
|
||||||
self.itemList.removeTopLevelItem(item.listItem)
|
listItem = item.listItem
|
||||||
|
listItem.canvasItem = None
|
||||||
|
item.listItem = None
|
||||||
|
self.itemList.removeTopLevelItem(listItem)
|
||||||
self.items.remove(item)
|
self.items.remove(item)
|
||||||
|
ctrl = item.ctrlWidget()
|
||||||
|
ctrl.hide()
|
||||||
|
self.ui.ctrlLayout.removeWidget(ctrl)
|
||||||
else:
|
else:
|
||||||
if hasattr(item, '_canvasItem'):
|
if hasattr(item, '_canvasItem'):
|
||||||
self.removeItem(item._canvasItem)
|
self.removeItem(item._canvasItem)
|
||||||
@ -555,7 +574,15 @@ class Canvas(QtGui.QWidget):
|
|||||||
#self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item)
|
#self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item)
|
||||||
self.sigItemTransformChangeFinished.emit(self, item)
|
self.sigItemTransformChangeFinished.emit(self, item)
|
||||||
|
|
||||||
|
def itemListContextMenuEvent(self, ev):
|
||||||
|
self.menuItem = self.itemList.itemAt(ev.pos())
|
||||||
|
self.menu.popup(ev.globalPos())
|
||||||
|
|
||||||
|
def removeClicked(self):
|
||||||
|
self.removeItem(self.menuItem)
|
||||||
|
self.menuItem = None
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
class SelectBox(ROI):
|
class SelectBox(ROI):
|
||||||
def __init__(self, scalable=False):
|
def __init__(self, scalable=False):
|
||||||
|
@ -210,6 +210,9 @@ class ConsoleWidget(QtGui.QWidget):
|
|||||||
#self.stdout.write(strn)
|
#self.stdout.write(strn)
|
||||||
|
|
||||||
def displayException(self):
|
def displayException(self):
|
||||||
|
"""
|
||||||
|
Display the current exception and stack.
|
||||||
|
"""
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
lines = []
|
lines = []
|
||||||
indent = 4
|
indent = 4
|
||||||
|
@ -8,7 +8,7 @@ Simple Data Display Functions
|
|||||||
|
|
||||||
.. autofunction:: pyqtgraph.image
|
.. autofunction:: pyqtgraph.image
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.dbg
|
||||||
|
|
||||||
Color, Pen, and Brush Functions
|
Color, Pen, and Brush Functions
|
||||||
-------------------------------
|
-------------------------------
|
||||||
@ -34,6 +34,8 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and
|
|||||||
|
|
||||||
.. autofunction:: pyqtgraph.colorStr
|
.. autofunction:: pyqtgraph.colorStr
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.glColor
|
||||||
|
|
||||||
|
|
||||||
Data Slicing
|
Data Slicing
|
||||||
------------
|
------------
|
||||||
@ -41,6 +43,18 @@ Data Slicing
|
|||||||
.. autofunction:: pyqtgraph.affineSlice
|
.. autofunction:: pyqtgraph.affineSlice
|
||||||
|
|
||||||
|
|
||||||
|
Coordinate Transformation
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.transformToArray
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.transformCoordinates
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.solve3DTransform
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.solveBilinearTransform
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SI Unit Conversion Functions
|
SI Unit Conversion Functions
|
||||||
----------------------------
|
----------------------------
|
||||||
@ -59,6 +73,12 @@ Image Preparation Functions
|
|||||||
|
|
||||||
.. autofunction:: pyqtgraph.makeQImage
|
.. autofunction:: pyqtgraph.makeQImage
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.applyLookupTable
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.rescaleData
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.imageToArray
|
||||||
|
|
||||||
|
|
||||||
Mesh Generation Functions
|
Mesh Generation Functions
|
||||||
-------------------------
|
-------------------------
|
||||||
@ -68,4 +88,13 @@ Mesh Generation Functions
|
|||||||
.. autofunction:: pyqtgraph.isosurface
|
.. autofunction:: pyqtgraph.isosurface
|
||||||
|
|
||||||
|
|
||||||
|
Miscellaneous Functions
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.pseudoScatter
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.systemInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,9 +45,9 @@ data = np.abs(np.fromfunction(psi, (50,50,100)))
|
|||||||
|
|
||||||
|
|
||||||
print("Generating isosurface..")
|
print("Generating isosurface..")
|
||||||
verts = pg.isosurface(data, data.max()/4.)
|
verts, faces = pg.isosurface(data, data.max()/4.)
|
||||||
|
|
||||||
md = gl.MeshData(vertexes=verts)
|
md = gl.MeshData(vertexes=verts, faces=faces)
|
||||||
|
|
||||||
colors = np.ones((md.faceCount(), 4), dtype=float)
|
colors = np.ones((md.faceCount(), 4), dtype=float)
|
||||||
colors[:,3] = 0.2
|
colors[:,3] = 0.2
|
||||||
|
@ -175,8 +175,11 @@ def testFile(name, f, exe, lib):
|
|||||||
try:
|
try:
|
||||||
%s
|
%s
|
||||||
import %s
|
import %s
|
||||||
|
import sys
|
||||||
print("test complete")
|
print("test complete")
|
||||||
|
sys.stdout.flush()
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
import time
|
||||||
while True: ## run a little event loop
|
while True: ## run a little event loop
|
||||||
pg.QtGui.QApplication.processEvents()
|
pg.QtGui.QApplication.processEvents()
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
@ -186,7 +189,7 @@ except:
|
|||||||
|
|
||||||
""" % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0])
|
""" % ("import %s" % lib if lib != '' else "", os.path.splitext(os.path.split(fn)[1])[0])
|
||||||
#print code
|
#print code
|
||||||
process = subprocess.Popen(['%s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
process.stdin.write(code.encode('UTF-8'))
|
process.stdin.write(code.encode('UTF-8'))
|
||||||
#process.stdin.close()
|
#process.stdin.close()
|
||||||
output = ''
|
output = ''
|
||||||
@ -202,10 +205,11 @@ except:
|
|||||||
fail = True
|
fail = True
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
process.terminate()
|
process.kill()
|
||||||
|
#process.wait()
|
||||||
res = process.communicate()
|
res = process.communicate()
|
||||||
#if 'exception' in res[1].lower() or 'error' in res[1].lower():
|
|
||||||
if fail:
|
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
||||||
print('.' * (50-len(name)) + 'FAILED')
|
print('.' * (50-len(name)) + 'FAILED')
|
||||||
print(res[0].decode())
|
print(res[0].decode())
|
||||||
print(res[1].decode())
|
print(res[1].decode())
|
||||||
|
@ -19,6 +19,7 @@ frames = 200
|
|||||||
data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
|
data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
|
||||||
data = np.concatenate([data, data], axis=0)
|
data = np.concatenate([data, data], axis=0)
|
||||||
data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2]
|
data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2]
|
||||||
|
data[:, 15:16, 15:17] += 1
|
||||||
|
|
||||||
win = pg.GraphicsWindow()
|
win = pg.GraphicsWindow()
|
||||||
vb = win.addViewBox()
|
vb = win.addViewBox()
|
||||||
|
@ -73,7 +73,8 @@ class Exporter(object):
|
|||||||
|
|
||||||
def getSourceRect(self):
|
def getSourceRect(self):
|
||||||
if isinstance(self.item, pg.GraphicsScene):
|
if isinstance(self.item, pg.GraphicsScene):
|
||||||
return self.item.getViewWidget().viewRect()
|
w = self.item.getViewWidget()
|
||||||
|
return w.viewportTransform().inverted()[0].mapRect(w.rect())
|
||||||
else:
|
else:
|
||||||
return self.item.sceneBoundingRect()
|
return self.item.sceneBoundingRect()
|
||||||
|
|
||||||
|
@ -36,11 +36,14 @@ class SVGExporter(Exporter):
|
|||||||
return
|
return
|
||||||
self.svg = QtSvg.QSvgGenerator()
|
self.svg = QtSvg.QSvgGenerator()
|
||||||
self.svg.setFileName(fileName)
|
self.svg.setFileName(fileName)
|
||||||
self.svg.setSize(QtCore.QSize(100,100))
|
dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||||
#self.svg.setResolution(600)
|
## not really sure why this works, but it seems to be important:
|
||||||
|
self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.))
|
||||||
|
self.svg.setResolution(dpi)
|
||||||
#self.svg.setViewBox()
|
#self.svg.setViewBox()
|
||||||
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
|
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
|
||||||
sourceRect = self.getSourceRect()
|
sourceRect = self.getSourceRect()
|
||||||
|
|
||||||
painter = QtGui.QPainter(self.svg)
|
painter = QtGui.QPainter(self.svg)
|
||||||
try:
|
try:
|
||||||
self.setExportMode(True)
|
self.setExportMode(True)
|
||||||
|
355
functions.py
355
functions.py
@ -491,9 +491,6 @@ def transformToArray(tr):
|
|||||||
## map coordinates through transform
|
## map coordinates through transform
|
||||||
mapped = np.dot(m, coords)
|
mapped = np.dot(m, coords)
|
||||||
"""
|
"""
|
||||||
if isinstance(tr, np.ndarray):
|
|
||||||
return tr
|
|
||||||
|
|
||||||
#return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
|
#return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
|
||||||
## The order of elements given by the method names m11..m33 is misleading--
|
## The order of elements given by the method names m11..m33 is misleading--
|
||||||
## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in
|
## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in
|
||||||
@ -506,18 +503,28 @@ def transformToArray(tr):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Transform argument must be either QTransform or QMatrix4x4.")
|
raise Exception("Transform argument must be either QTransform or QMatrix4x4.")
|
||||||
|
|
||||||
def transformCoordinates(tr, coords):
|
def transformCoordinates(tr, coords, transpose=False):
|
||||||
"""
|
"""
|
||||||
Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4.
|
Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4.
|
||||||
The shape of coords must be (2,...) or (3,...)
|
The shape of coords must be (2,...) or (3,...)
|
||||||
The mapping will _ignore_ any perspective transformations.
|
The mapping will _ignore_ any perspective transformations.
|
||||||
|
|
||||||
|
For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication.
|
||||||
|
Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To
|
||||||
|
allow this, use transpose=True.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if transpose:
|
||||||
|
## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates.
|
||||||
|
coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1)))
|
||||||
|
|
||||||
nd = coords.shape[0]
|
nd = coords.shape[0]
|
||||||
if not isinstance(tr, np.ndarray):
|
if isinstance(tr, np.ndarray):
|
||||||
|
m = tr
|
||||||
|
else:
|
||||||
m = transformToArray(tr)
|
m = transformToArray(tr)
|
||||||
m = m[:m.shape[0]-1] # remove perspective
|
m = m[:m.shape[0]-1] # remove perspective
|
||||||
else:
|
|
||||||
m = tr
|
|
||||||
|
|
||||||
## If coords are 3D and tr is 2D, assume no change for Z axis
|
## If coords are 3D and tr is 2D, assume no change for Z axis
|
||||||
if m.shape == (2,3) and nd == 3:
|
if m.shape == (2,3) and nd == 3:
|
||||||
@ -545,9 +552,15 @@ def transformCoordinates(tr, coords):
|
|||||||
## map coordinates and return
|
## map coordinates and return
|
||||||
mapped = (m*coords).sum(axis=1) ## apply scale/rotate
|
mapped = (m*coords).sum(axis=1) ## apply scale/rotate
|
||||||
mapped += translate
|
mapped += translate
|
||||||
|
|
||||||
|
if transpose:
|
||||||
|
## move first axis to end.
|
||||||
|
mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,))
|
||||||
return mapped
|
return mapped
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def solve3DTransform(points1, points2):
|
def solve3DTransform(points1, points2):
|
||||||
"""
|
"""
|
||||||
Find a 3D transformation matrix that maps points1 onto points2
|
Find a 3D transformation matrix that maps points1 onto points2
|
||||||
@ -782,7 +795,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
|||||||
if levels.shape != (data.shape[-1], 2):
|
if levels.shape != (data.shape[-1], 2):
|
||||||
raise Exception('levels must have shape (data.shape[-1], 2)')
|
raise Exception('levels must have shape (data.shape[-1], 2)')
|
||||||
else:
|
else:
|
||||||
print(levels)
|
print levels
|
||||||
raise Exception("levels argument must be 1D or 2D.")
|
raise Exception("levels argument must be 1D or 2D.")
|
||||||
#levels = np.array(levels)
|
#levels = np.array(levels)
|
||||||
#if levels.shape == (2,):
|
#if levels.shape == (2,):
|
||||||
@ -1066,16 +1079,43 @@ def imageToArray(img, copy=False, transpose=True):
|
|||||||
#return facets
|
#return facets
|
||||||
|
|
||||||
|
|
||||||
def isocurve(data, level):
|
def isocurve(data, level, connected=False, extendToEdge=False, path=False):
|
||||||
"""
|
"""
|
||||||
Generate isocurve from 2D data using marching squares algorithm.
|
Generate isocurve from 2D data using marching squares algorithm.
|
||||||
|
|
||||||
*data* 2D numpy array of scalar values
|
============= =========================================================
|
||||||
*level* The level at which to generate an isosurface
|
Arguments
|
||||||
|
data 2D numpy array of scalar values
|
||||||
|
level The level at which to generate an isosurface
|
||||||
|
connected If False, return a single long list of point pairs
|
||||||
|
If True, return multiple long lists of connected point
|
||||||
|
locations. (This is slower but better for drawing
|
||||||
|
continuous lines)
|
||||||
|
extendToEdge If True, extend the curves to reach the exact edges of
|
||||||
|
the data.
|
||||||
|
path if True, return a QPainterPath rather than a list of
|
||||||
|
vertex coordinates. This forces connected=True.
|
||||||
|
============= =========================================================
|
||||||
|
|
||||||
This function is SLOW; plenty of room for optimization here.
|
This function is SLOW; plenty of room for optimization here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if path is True:
|
||||||
|
connected = True
|
||||||
|
|
||||||
|
if extendToEdge:
|
||||||
|
d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype)
|
||||||
|
d2[1:-1, 1:-1] = data
|
||||||
|
d2[0, 1:-1] = data[0]
|
||||||
|
d2[-1, 1:-1] = data[-1]
|
||||||
|
d2[1:-1, 0] = data[:, 0]
|
||||||
|
d2[1:-1, -1] = data[:, -1]
|
||||||
|
d2[0,0] = d2[0,1]
|
||||||
|
d2[0,-1] = d2[1,-1]
|
||||||
|
d2[-1,0] = d2[-1,1]
|
||||||
|
d2[-1,-1] = d2[-1,-2]
|
||||||
|
data = d2
|
||||||
|
|
||||||
sideTable = [
|
sideTable = [
|
||||||
[],
|
[],
|
||||||
[0,1],
|
[0,1],
|
||||||
@ -1096,7 +1136,7 @@ def isocurve(data, level):
|
|||||||
]
|
]
|
||||||
|
|
||||||
edgeKey=[
|
edgeKey=[
|
||||||
[(0,1),(0,0)],
|
[(0,1), (0,0)],
|
||||||
[(0,0), (1,0)],
|
[(0,0), (1,0)],
|
||||||
[(1,0), (1,1)],
|
[(1,0), (1,1)],
|
||||||
[(1,1), (0,1)]
|
[(1,1), (0,1)]
|
||||||
@ -1140,31 +1180,150 @@ def isocurve(data, level):
|
|||||||
p1[0]*fi + p2[0]*f + i + 0.5,
|
p1[0]*fi + p2[0]*f + i + 0.5,
|
||||||
p1[1]*fi + p2[1]*f + j + 0.5
|
p1[1]*fi + p2[1]*f + j + 0.5
|
||||||
)
|
)
|
||||||
|
if extendToEdge:
|
||||||
|
## check bounds
|
||||||
|
p = (
|
||||||
|
min(data.shape[0]-2, max(0, p[0]-1)),
|
||||||
|
min(data.shape[1]-2, max(0, p[1]-1)),
|
||||||
|
)
|
||||||
|
if connected:
|
||||||
|
gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2
|
||||||
|
pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments)
|
||||||
|
else:
|
||||||
pts.append(p)
|
pts.append(p)
|
||||||
|
|
||||||
lines.append(pts)
|
lines.append(pts)
|
||||||
|
|
||||||
|
if not connected:
|
||||||
|
return lines
|
||||||
|
|
||||||
|
## turn disjoint list of segments into continuous lines
|
||||||
|
|
||||||
|
#lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]]
|
||||||
|
#lines = [[(float(a), a), (float(b), b)] for a,b in lines]
|
||||||
|
points = {} ## maps each point to its connections
|
||||||
|
for a,b in lines:
|
||||||
|
if a[1] not in points:
|
||||||
|
points[a[1]] = []
|
||||||
|
points[a[1]].append([a,b])
|
||||||
|
if b[1] not in points:
|
||||||
|
points[b[1]] = []
|
||||||
|
points[b[1]].append([b,a])
|
||||||
|
|
||||||
|
## rearrange into chains
|
||||||
|
for k in points.keys():
|
||||||
|
try:
|
||||||
|
chains = points[k]
|
||||||
|
except KeyError: ## already used this point elsewhere
|
||||||
|
continue
|
||||||
|
#print "===========", k
|
||||||
|
for chain in chains:
|
||||||
|
#print " chain:", chain
|
||||||
|
x = None
|
||||||
|
while True:
|
||||||
|
if x == chain[-1][1]:
|
||||||
|
break ## nothing left to do on this chain
|
||||||
|
|
||||||
|
x = chain[-1][1]
|
||||||
|
if x == k:
|
||||||
|
break ## chain has looped; we're done and can ignore the opposite chain
|
||||||
|
y = chain[-2][1]
|
||||||
|
connects = points[x]
|
||||||
|
for conn in connects[:]:
|
||||||
|
if conn[1][1] != y:
|
||||||
|
#print " ext:", conn
|
||||||
|
chain.extend(conn[1:])
|
||||||
|
#print " del:", x
|
||||||
|
del points[x]
|
||||||
|
if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction
|
||||||
|
chains.pop()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
## extract point locations
|
||||||
|
lines = []
|
||||||
|
for chain in points.values():
|
||||||
|
if len(chain) == 2:
|
||||||
|
chain = chain[1][1:][::-1] + chain[0] # join together ends of chain
|
||||||
|
else:
|
||||||
|
chain = chain[0]
|
||||||
|
lines.append([p[0] for p in chain])
|
||||||
|
|
||||||
|
if not path:
|
||||||
return lines ## a list of pairs of points
|
return lines ## a list of pairs of points
|
||||||
|
|
||||||
|
path = QtGui.QPainterPath()
|
||||||
|
for line in lines:
|
||||||
|
path.moveTo(*line[0])
|
||||||
|
for p in line[1:]:
|
||||||
|
path.lineTo(*p)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def traceImage(image, values, smooth=0.5):
|
||||||
|
"""
|
||||||
|
Convert an image to a set of QPainterPath curves.
|
||||||
|
One curve will be generated for each item in *values*; each curve outlines the area
|
||||||
|
of the image that is closer to its value than to any others.
|
||||||
|
|
||||||
|
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
|
||||||
|
The parameter *smooth* is expressed in pixels.
|
||||||
|
"""
|
||||||
|
import scipy.ndimage as ndi
|
||||||
|
if values.ndim == 2:
|
||||||
|
values = values.T
|
||||||
|
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
||||||
|
image = image[..., np.newaxis].astype(float)
|
||||||
|
diff = np.abs(image-values)
|
||||||
|
if values.ndim == 4:
|
||||||
|
diff = diff.sum(axis=2)
|
||||||
|
|
||||||
|
labels = np.argmin(diff, axis=2)
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for i in range(diff.shape[-1]):
|
||||||
|
d = (labels==i).astype(float)
|
||||||
|
d = ndi.gaussian_filter(d, (smooth, smooth))
|
||||||
|
lines = isocurve(d, 0.5, connected=True, extendToEdge=True)
|
||||||
|
path = QtGui.QPainterPath()
|
||||||
|
for line in lines:
|
||||||
|
path.moveTo(*line[0])
|
||||||
|
for p in line[1:]:
|
||||||
|
path.lineTo(*p)
|
||||||
|
|
||||||
|
paths.append(path)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
IsosurfaceDataCache = None
|
||||||
def isosurface(data, level):
|
def isosurface(data, level):
|
||||||
"""
|
"""
|
||||||
Generate isosurface from volumetric data using marching cubes algorithm.
|
Generate isosurface from volumetric data using marching cubes algorithm.
|
||||||
See Paul Bourke, "Polygonising a Scalar Field"
|
See Paul Bourke, "Polygonising a Scalar Field"
|
||||||
(http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/)
|
(http://paulbourke.net/geometry/polygonise/)
|
||||||
|
|
||||||
*data* 3D numpy array of scalar values
|
*data* 3D numpy array of scalar values
|
||||||
*level* The level at which to generate an isosurface
|
*level* The level at which to generate an isosurface
|
||||||
|
|
||||||
Returns an array of vertex coordinates (N, 3, 3);
|
Returns an array of vertex coordinates (Nv, 3) and an array of
|
||||||
|
per-face vertex indexes (Nf, 3)
|
||||||
This function is SLOW; plenty of room for optimization here.
|
|
||||||
"""
|
"""
|
||||||
|
## For improvement, see:
|
||||||
|
##
|
||||||
|
## Efficient implementation of Marching Cubes' cases with topological guarantees.
|
||||||
|
## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares.
|
||||||
|
## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003)
|
||||||
|
|
||||||
|
## Precompute lookup tables on the first run
|
||||||
|
global IsosurfaceDataCache
|
||||||
|
if IsosurfaceDataCache is None:
|
||||||
## map from grid cell index to edge index.
|
## map from grid cell index to edge index.
|
||||||
## grid cell index tells us which corners are below the isosurface,
|
## grid cell index tells us which corners are below the isosurface,
|
||||||
## edge index tells us which edges are cut by the isosurface.
|
## edge index tells us which edges are cut by the isosurface.
|
||||||
## (Data stolen from Bourk; see above.)
|
## (Data stolen from Bourk; see above.)
|
||||||
edgeTable = [
|
edgeTable = np.array([
|
||||||
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
||||||
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
||||||
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
||||||
@ -1196,7 +1355,7 @@ def isosurface(data, level):
|
|||||||
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
||||||
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
|
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
|
||||||
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
||||||
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ]
|
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16)
|
||||||
|
|
||||||
## Table of triangles to use for filling each grid cell.
|
## Table of triangles to use for filling each grid cell.
|
||||||
## Each set of three integers tells us which three edges to
|
## Each set of three integers tells us which three edges to
|
||||||
@ -1460,27 +1619,43 @@ def isosurface(data, level):
|
|||||||
[0, 3, 8],
|
[0, 3, 8],
|
||||||
[]
|
[]
|
||||||
]
|
]
|
||||||
|
edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2)
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 0, 0, 1],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[1, 0, 1, 1],
|
||||||
|
[0, 1, 1, 0],
|
||||||
|
[0, 0, 1, 1],
|
||||||
|
[0, 0, 0, 2],
|
||||||
|
[1, 0, 0, 2],
|
||||||
|
[1, 1, 0, 2],
|
||||||
|
[0, 1, 0, 2],
|
||||||
|
#[9, 9, 9, 9] ## fake
|
||||||
|
], dtype=np.ubyte)
|
||||||
|
nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte)
|
||||||
|
faceShiftTables = [None]
|
||||||
|
for i in range(1,6):
|
||||||
|
## compute lookup table of index: vertexes mapping
|
||||||
|
faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte)
|
||||||
|
faceTableInds = np.argwhere(nTableFaces == i)
|
||||||
|
faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds])
|
||||||
|
faceTableI = faceTableI.reshape((len(triTable), i, 3))
|
||||||
|
faceShiftTables.append(edgeShifts[faceTableI])
|
||||||
|
|
||||||
## translation between edge index and
|
## Let's try something different:
|
||||||
## the vertex indexes that bound the edge
|
#faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup)
|
||||||
edgeKey = [
|
#for i,f in enumerate(triTable):
|
||||||
[(0,0,0), (1,0,0)],
|
#f = np.array(f + [12] * (15-len(f))).reshape(5,3)
|
||||||
[(1,0,0), (1,1,0)],
|
#faceTable[i] = edgeShifts[f]
|
||||||
[(1,1,0), (0,1,0)],
|
|
||||||
[(0,1,0), (0,0,0)],
|
|
||||||
[(0,0,1), (1,0,1)],
|
|
||||||
[(1,0,1), (1,1,1)],
|
|
||||||
[(1,1,1), (0,1,1)],
|
|
||||||
[(0,1,1), (0,0,1)],
|
|
||||||
[(0,0,0), (0,0,1)],
|
|
||||||
[(1,0,0), (1,0,1)],
|
|
||||||
[(1,1,0), (1,1,1)],
|
|
||||||
[(0,1,0), (0,1,1)],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces)
|
||||||
|
else:
|
||||||
|
faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache
|
||||||
|
|
||||||
|
|
||||||
facets = []
|
|
||||||
|
|
||||||
## mark everything below the isosurface level
|
## mark everything below the isosurface level
|
||||||
mask = data < level
|
mask = data < level
|
||||||
@ -1494,35 +1669,93 @@ def isosurface(data, level):
|
|||||||
for k in [0,1]:
|
for k in [0,1]:
|
||||||
fields[i,j,k] = mask[slices[i], slices[j], slices[k]]
|
fields[i,j,k] = mask[slices[i], slices[j], slices[k]]
|
||||||
vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme
|
vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme
|
||||||
#print i,j,k," : ", fields[i,j,k], 2**vertIndex
|
|
||||||
index += fields[i,j,k] * 2**vertIndex
|
index += fields[i,j,k] * 2**vertIndex
|
||||||
#print index
|
|
||||||
#print index
|
|
||||||
|
|
||||||
## add facets
|
### Generate table of edges that have been cut
|
||||||
for i in range(index.shape[0]): # data x-axis
|
cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32)
|
||||||
for j in range(index.shape[1]): # data y-axis
|
edges = edgeTable[index]
|
||||||
for k in range(index.shape[2]): # data z-axis
|
for i, shift in enumerate(edgeShifts[:12]):
|
||||||
tris = triTable[index[i,j,k]]
|
slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)]
|
||||||
for l in range(0, len(tris), 3): ## faces for this grid cell
|
cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i
|
||||||
edges = tris[l:l+3]
|
|
||||||
pts = []
|
|
||||||
for m in [0,1,2]: # points in this face
|
|
||||||
p1 = edgeKey[edges[m]][0]
|
|
||||||
p2 = edgeKey[edges[m]][1]
|
|
||||||
v1 = data[i+p1[0], j+p1[1], k+p1[2]]
|
|
||||||
v2 = data[i+p2[0], j+p2[1], k+p2[2]]
|
|
||||||
f = (level-v1) / (v2-v1)
|
|
||||||
fi = 1.0 - f
|
|
||||||
p = ( ## interpolate between corners
|
|
||||||
p1[0]*fi + p2[0]*f + i + 0.5,
|
|
||||||
p1[1]*fi + p2[1]*f + j + 0.5,
|
|
||||||
p1[2]*fi + p2[2]*f + k + 0.5
|
|
||||||
)
|
|
||||||
pts.append(p)
|
|
||||||
facets.append(pts)
|
|
||||||
|
|
||||||
return np.array(facets)
|
## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions
|
||||||
|
m = cutEdges > 0
|
||||||
|
vertexInds = np.argwhere(m) ## argwhere is slow!
|
||||||
|
vertexes = vertexInds[:,:3].astype(np.float32)
|
||||||
|
dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2])
|
||||||
|
|
||||||
|
## re-use the cutEdges array as a lookup table for vertex IDs
|
||||||
|
cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0])
|
||||||
|
|
||||||
|
for i in [0,1,2]:
|
||||||
|
vim = vertexInds[:,3] == i
|
||||||
|
vi = vertexInds[vim, :3]
|
||||||
|
viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1)
|
||||||
|
v1 = dataFlat[viFlat]
|
||||||
|
v2 = dataFlat[viFlat + data.strides[i]/data.itemsize]
|
||||||
|
vertexes[vim,i] += (level-v1) / (v2-v1)
|
||||||
|
|
||||||
|
### compute the set of vertex indexes for each face.
|
||||||
|
|
||||||
|
## This works, but runs a bit slower.
|
||||||
|
#cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face
|
||||||
|
#cellInds = index[cells[:,0], cells[:,1], cells[:,2]]
|
||||||
|
#verts = faceTable[cellInds]
|
||||||
|
#mask = verts[...,0,0] != 9
|
||||||
|
#verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges
|
||||||
|
#verts = verts[mask]
|
||||||
|
#faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
|
||||||
|
|
||||||
|
|
||||||
|
## To allow this to be vectorized efficiently, we count the number of faces in each
|
||||||
|
## grid cell and handle each group of cells with the same number together.
|
||||||
|
## determine how many faces to assign to each grid cell
|
||||||
|
nFaces = nTableFaces[index]
|
||||||
|
totFaces = nFaces.sum()
|
||||||
|
faces = np.empty((totFaces, 3), dtype=np.uint32)
|
||||||
|
ptr = 0
|
||||||
|
#import debug
|
||||||
|
#p = debug.Profiler('isosurface', disabled=False)
|
||||||
|
|
||||||
|
## this helps speed up an indexing operation later on
|
||||||
|
cs = np.array(cutEdges.strides)/cutEdges.itemsize
|
||||||
|
cutEdges = cutEdges.flatten()
|
||||||
|
|
||||||
|
## this, strangely, does not seem to help.
|
||||||
|
#ins = np.array(index.strides)/index.itemsize
|
||||||
|
#index = index.flatten()
|
||||||
|
|
||||||
|
for i in range(1,6):
|
||||||
|
### expensive:
|
||||||
|
#p.mark('1')
|
||||||
|
cells = np.argwhere(nFaces == i) ## all cells which require i faces (argwhere is expensive)
|
||||||
|
#p.mark('2')
|
||||||
|
if cells.shape[0] == 0:
|
||||||
|
continue
|
||||||
|
#cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)]
|
||||||
|
cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round
|
||||||
|
#p.mark('3')
|
||||||
|
|
||||||
|
### expensive:
|
||||||
|
verts = faceShiftTables[i][cellInds]
|
||||||
|
#p.mark('4')
|
||||||
|
verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges
|
||||||
|
verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:])
|
||||||
|
#p.mark('5')
|
||||||
|
|
||||||
|
### expensive:
|
||||||
|
#print verts.shape
|
||||||
|
verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2)
|
||||||
|
#vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
|
||||||
|
vertInds = cutEdges[verts]
|
||||||
|
#p.mark('6')
|
||||||
|
nv = vertInds.shape[0]
|
||||||
|
#p.mark('7')
|
||||||
|
faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3))
|
||||||
|
#p.mark('8')
|
||||||
|
ptr += nv
|
||||||
|
|
||||||
|
return vertexes, faces
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene
|
|||||||
from pyqtgraph.Point import Point
|
from pyqtgraph.Point import Point
|
||||||
import pyqtgraph.functions as fn
|
import pyqtgraph.functions as fn
|
||||||
import weakref
|
import weakref
|
||||||
|
import operator
|
||||||
|
|
||||||
class GraphicsItem(object):
|
class GraphicsItem(object):
|
||||||
"""
|
"""
|
||||||
@ -395,8 +396,16 @@ class GraphicsItem(object):
|
|||||||
## disconnect from previous view
|
## disconnect from previous view
|
||||||
if oldView is not None:
|
if oldView is not None:
|
||||||
#print "disconnect:", self, oldView
|
#print "disconnect:", self, oldView
|
||||||
|
try:
|
||||||
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
|
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
|
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
self._connectedView = None
|
self._connectedView = None
|
||||||
|
|
||||||
## connect to new view
|
## connect to new view
|
||||||
@ -450,3 +459,21 @@ class GraphicsItem(object):
|
|||||||
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
|
if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
|
||||||
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
|
||||||
|
|
||||||
|
def childrenShape(self):
|
||||||
|
"""Return the union of the shapes of all descendants of this item in local coordinates."""
|
||||||
|
childs = self.allChildItems()
|
||||||
|
shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()]
|
||||||
|
return reduce(operator.add, shapes)
|
||||||
|
|
||||||
|
def allChildItems(self, root=None):
|
||||||
|
"""Return list of the entire item tree descending from this item."""
|
||||||
|
if root is None:
|
||||||
|
root = self
|
||||||
|
tree = []
|
||||||
|
for ch in root.childItems():
|
||||||
|
tree.append(ch)
|
||||||
|
tree.extend(self.allChildItems(ch))
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
|||||||
from pyqtgraph.Qt import QtGui, QtCore
|
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
|
||||||
|
if not USE_PYSIDE:
|
||||||
|
import sip
|
||||||
from .GraphicsItem import GraphicsItem
|
from .GraphicsItem import GraphicsItem
|
||||||
|
|
||||||
__all__ = ['GraphicsObject']
|
__all__ = ['GraphicsObject']
|
||||||
@ -20,4 +22,10 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
|
|||||||
self._updateView()
|
self._updateView()
|
||||||
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
|
## workaround for pyqt bug:
|
||||||
|
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
|
||||||
|
if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem):
|
||||||
|
ret = sip.cast(ret, QtGui.QGraphicsItem)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
|
|||||||
"""
|
"""
|
||||||
Set the data/image to draw isocurves for.
|
Set the data/image to draw isocurves for.
|
||||||
|
|
||||||
============= ================================================================
|
============= ========================================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
data A 2-dimensional ndarray.
|
data A 2-dimensional ndarray.
|
||||||
level The cutoff value at which to draw the curve. If level is not specified,
|
level The cutoff value at which to draw the curve. If level is not specified,
|
||||||
the previous level is used.
|
the previously set level is used.
|
||||||
============= ================================================================
|
============= ========================================================================
|
||||||
"""
|
"""
|
||||||
if level is None:
|
if level is None:
|
||||||
level = self.level
|
level = self.level
|
||||||
@ -74,6 +74,12 @@ class IsocurveItem(GraphicsObject):
|
|||||||
self.pen = fn.mkPen(*args, **kwargs)
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def setBrush(self, *args, **kwargs):
|
||||||
|
"""Set the brush used to draw the isocurve. Arguments can be any that are valid
|
||||||
|
for :func:`mkBrush <pyqtgraph.mkBrush>`"""
|
||||||
|
self.brush = fn.mkBrush(*args, **kwargs)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
def updateLines(self, data, level):
|
def updateLines(self, data, level):
|
||||||
##print "data:", data
|
##print "data:", data
|
||||||
@ -88,20 +94,26 @@ class IsocurveItem(GraphicsObject):
|
|||||||
self.setData(data, level)
|
self.setData(data, level)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
if self.path is None:
|
if self.data is None:
|
||||||
return QtCore.QRectF()
|
return QtCore.QRectF()
|
||||||
|
if self.path is None:
|
||||||
|
self.generatePath()
|
||||||
return self.path.boundingRect()
|
return self.path.boundingRect()
|
||||||
|
|
||||||
def generatePath(self):
|
def generatePath(self):
|
||||||
self.path = QtGui.QPainterPath()
|
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
|
self.path = None
|
||||||
return
|
return
|
||||||
lines = fn.isocurve(self.data, self.level)
|
lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True)
|
||||||
|
self.path = QtGui.QPainterPath()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
self.path.moveTo(*line[0])
|
self.path.moveTo(*line[0])
|
||||||
self.path.lineTo(*line[1])
|
for p in line[1:]:
|
||||||
|
self.path.lineTo(*p)
|
||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
|
if self.data is None:
|
||||||
|
return
|
||||||
if self.path is None:
|
if self.path is None:
|
||||||
self.generatePath()
|
self.generatePath()
|
||||||
p.setPen(self.pen)
|
p.setPen(self.pen)
|
||||||
|
@ -233,7 +233,7 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
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
|
||||||
self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
|
self.opts = {'pxMode': True, 'useCache': True, 'exportMode': False} ## If useCache is False, symbols are re-drawn on every paint.
|
||||||
|
|
||||||
self.setPen(200,200,200, update=False)
|
self.setPen(200,200,200, update=False)
|
||||||
self.setBrush(100,100,150, update=False)
|
self.setBrush(100,100,150, update=False)
|
||||||
@ -664,10 +664,14 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
rect = QtCore.QRectF(y, x, h, w)
|
rect = QtCore.QRectF(y, x, h, w)
|
||||||
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
||||||
|
|
||||||
|
def setExportMode(self, enabled, opts):
|
||||||
|
self.opts['exportMode'] = enabled
|
||||||
|
|
||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
#p.setPen(fn.mkPen('r'))
|
#p.setPen(fn.mkPen('r'))
|
||||||
#p.drawRect(self.boundingRect())
|
#p.drawRect(self.boundingRect())
|
||||||
if self.opts['pxMode']:
|
if self.opts['pxMode'] is True:
|
||||||
atlas = self.fragmentAtlas.getAtlas()
|
atlas = self.fragmentAtlas.getAtlas()
|
||||||
#arr = fn.imageToArray(atlas.toImage(), copy=True)
|
#arr = fn.imageToArray(atlas.toImage(), copy=True)
|
||||||
#if hasattr(self, 'lastAtlas'):
|
#if hasattr(self, 'lastAtlas'):
|
||||||
@ -681,7 +685,7 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
|
|
||||||
p.resetTransform()
|
p.resetTransform()
|
||||||
|
|
||||||
if not USE_PYSIDE and self.opts['useCache']:
|
if not USE_PYSIDE and self.opts['useCache'] and self.opts['exportMode'] is False:
|
||||||
p.drawPixmapFragments(self.fragments, atlas)
|
p.drawPixmapFragments(self.fragments, atlas)
|
||||||
else:
|
else:
|
||||||
for i in range(len(self.data)):
|
for i in range(len(self.data)):
|
||||||
|
@ -7,7 +7,7 @@ class TextItem(UIGraphicsItem):
|
|||||||
"""
|
"""
|
||||||
GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox).
|
GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox).
|
||||||
"""
|
"""
|
||||||
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None):
|
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
|
||||||
"""
|
"""
|
||||||
=========== =================================================================================
|
=========== =================================================================================
|
||||||
Arguments:
|
Arguments:
|
||||||
@ -22,6 +22,12 @@ class TextItem(UIGraphicsItem):
|
|||||||
*fill* A brush to use when filling within the border
|
*fill* A brush to use when filling within the border
|
||||||
=========== =================================================================================
|
=========== =================================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
## not working yet
|
||||||
|
#*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's
|
||||||
|
#transformation will be ignored)
|
||||||
|
|
||||||
|
|
||||||
UIGraphicsItem.__init__(self)
|
UIGraphicsItem.__init__(self)
|
||||||
self.textItem = QtGui.QGraphicsTextItem()
|
self.textItem = QtGui.QGraphicsTextItem()
|
||||||
self.lastTransform = None
|
self.lastTransform = None
|
||||||
@ -33,6 +39,7 @@ class TextItem(UIGraphicsItem):
|
|||||||
self.anchor = pg.Point(anchor)
|
self.anchor = pg.Point(anchor)
|
||||||
self.fill = pg.mkBrush(fill)
|
self.fill = pg.mkBrush(fill)
|
||||||
self.border = pg.mkPen(border)
|
self.border = pg.mkPen(border)
|
||||||
|
self.angle = angle
|
||||||
#self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
|
#self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
|
||||||
|
|
||||||
def setText(self, text, color=(200,200,200)):
|
def setText(self, text, color=(200,200,200)):
|
||||||
@ -115,9 +122,11 @@ class TextItem(UIGraphicsItem):
|
|||||||
|
|
||||||
#p.fillRect(tbr)
|
#p.fillRect(tbr)
|
||||||
p.resetTransform()
|
p.resetTransform()
|
||||||
p.drawRect(tbr)
|
#p.drawRect(tbr)
|
||||||
|
|
||||||
|
|
||||||
p.translate(tbr.left(), tbr.top())
|
p.translate(tbr.left(), tbr.top())
|
||||||
|
p.rotate(self.angle)
|
||||||
|
p.drawRect(QtCore.QRectF(0, 0, tbr.width(), tbr.height()))
|
||||||
self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
|
self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
|
||||||
|
|
@ -1,6 +1,8 @@
|
|||||||
from pyqtgraph.Qt import QtGui, QtCore
|
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
|
||||||
import weakref
|
import weakref
|
||||||
from .GraphicsObject import GraphicsObject
|
from .GraphicsObject import GraphicsObject
|
||||||
|
if not USE_PYSIDE:
|
||||||
|
import sip
|
||||||
|
|
||||||
__all__ = ['UIGraphicsItem']
|
__all__ = ['UIGraphicsItem']
|
||||||
class UIGraphicsItem(GraphicsObject):
|
class UIGraphicsItem(GraphicsObject):
|
||||||
@ -44,9 +46,12 @@ class UIGraphicsItem(GraphicsObject):
|
|||||||
|
|
||||||
def itemChange(self, change, value):
|
def itemChange(self, change, value):
|
||||||
ret = GraphicsObject.itemChange(self, change, value)
|
ret = GraphicsObject.itemChange(self, change, value)
|
||||||
#if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: ## handled by GraphicsItem now.
|
|
||||||
##print "caught parent/scene change:", self.parentItem(), self.scene()
|
## workaround for pyqt bug:
|
||||||
#self.updateView()
|
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
|
||||||
|
if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem):
|
||||||
|
ret = sip.cast(ret, QtGui.QGraphicsItem)
|
||||||
|
|
||||||
if change == self.ItemScenePositionHasChanged:
|
if change == self.ItemScenePositionHasChanged:
|
||||||
self.setNewBounds()
|
self.setNewBounds()
|
||||||
return ret
|
return ret
|
||||||
|
@ -111,6 +111,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
}
|
}
|
||||||
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
||||||
|
|
||||||
|
self.locateGroup = None ## items displayed when using ViewBox.locate(item)
|
||||||
|
|
||||||
self.setFlag(self.ItemClipsChildrenToShape)
|
self.setFlag(self.ItemClipsChildrenToShape)
|
||||||
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
|
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
|
||||||
@ -286,6 +287,12 @@ class ViewBox(GraphicsWidget):
|
|||||||
self.scene().removeItem(item)
|
self.scene().removeItem(item)
|
||||||
self.updateAutoRange()
|
self.updateAutoRange()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
for i in self.addedItems[:]:
|
||||||
|
self.removeItem(i)
|
||||||
|
for ch in self.childGroup.childItems():
|
||||||
|
ch.setParent(None)
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
#self.setRange(self.range, padding=0)
|
#self.setRange(self.range, padding=0)
|
||||||
#self.updateAutoRange()
|
#self.updateAutoRange()
|
||||||
@ -1232,5 +1239,45 @@ class ViewBox(GraphicsWidget):
|
|||||||
except RuntimeError: ## signal is already disconnected.
|
except RuntimeError: ## signal is already disconnected.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def locate(self, item, timeout=3.0, children=False):
|
||||||
|
"""
|
||||||
|
Temporarily display the bounding rect of an item and lines connecting to the center of the view.
|
||||||
|
This is useful for determining the location of items that may be out of the range of the ViewBox.
|
||||||
|
if allChildren is True, then the bounding rect of all item's children will be shown instead.
|
||||||
|
"""
|
||||||
|
self.clearLocate()
|
||||||
|
|
||||||
|
if item.scene() is not self.scene():
|
||||||
|
raise Exception("Item does not share a scene with this ViewBox.")
|
||||||
|
|
||||||
|
c = self.viewRect().center()
|
||||||
|
if children:
|
||||||
|
br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect()
|
||||||
|
else:
|
||||||
|
br = self.mapFromItemToView(item, item.boundingRect()).boundingRect()
|
||||||
|
|
||||||
|
g = ItemGroup()
|
||||||
|
g.setParentItem(self.childGroup)
|
||||||
|
self.locateGroup = g
|
||||||
|
g.box = QtGui.QGraphicsRectItem(br)
|
||||||
|
g.box.setParentItem(g)
|
||||||
|
g.lines = []
|
||||||
|
for p in (br.topLeft(), br.bottomLeft(), br.bottomRight(), br.topRight()):
|
||||||
|
line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y())
|
||||||
|
line.setParentItem(g)
|
||||||
|
g.lines.append(line)
|
||||||
|
|
||||||
|
for item in g.childItems():
|
||||||
|
item.setPen(fn.mkPen(color='y', width=3))
|
||||||
|
item.setZValue(1000000)
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(timeout*1000, self.clearLocate)
|
||||||
|
|
||||||
|
def clearLocate(self):
|
||||||
|
if self.locateGroup is None:
|
||||||
|
return
|
||||||
|
self.scene().removeItem(self.locateGroup)
|
||||||
|
self.locateGroup = None
|
||||||
|
|
||||||
|
|
||||||
from .ViewBoxMenu import ViewBoxMenu
|
from .ViewBoxMenu import ViewBoxMenu
|
||||||
|
@ -158,7 +158,7 @@ class GLMeshItem(GLGraphicsItem):
|
|||||||
if self.colors is None:
|
if self.colors is None:
|
||||||
color = self.opts['color']
|
color = self.opts['color']
|
||||||
if isinstance(color, QtGui.QColor):
|
if isinstance(color, QtGui.QColor):
|
||||||
glColor4f(*fn.glColor(color))
|
glColor4f(*pg.glColor(color))
|
||||||
else:
|
else:
|
||||||
glColor4f(*color)
|
glColor4f(*color)
|
||||||
else:
|
else:
|
||||||
|
@ -53,6 +53,7 @@ if sys.version_info[0] == 3:
|
|||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
builtins.cmp = cmp
|
builtins.cmp = cmp
|
||||||
|
builtins.xrange = range
|
||||||
#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
|
#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe
|
||||||
#import __builtin__
|
#import __builtin__
|
||||||
#__builtin__.asUnicode = asUnicode
|
#__builtin__.asUnicode = asUnicode
|
||||||
|
12
reload.py
12
reload.py
@ -35,6 +35,7 @@ def reloadAll(prefix=None, debug=False):
|
|||||||
- if prefix is None, checks all loaded modules
|
- if prefix is None, checks all loaded modules
|
||||||
"""
|
"""
|
||||||
failed = []
|
failed = []
|
||||||
|
changed = []
|
||||||
for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
|
for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload
|
||||||
if not inspect.ismodule(mod):
|
if not inspect.ismodule(mod):
|
||||||
continue
|
continue
|
||||||
@ -50,10 +51,11 @@ def reloadAll(prefix=None, debug=False):
|
|||||||
## ignore if the .pyc is newer than the .py (or if there is no pyc or py)
|
## ignore if the .pyc is newer than the .py (or if there is no pyc or py)
|
||||||
py = os.path.splitext(mod.__file__)[0] + '.py'
|
py = os.path.splitext(mod.__file__)[0] + '.py'
|
||||||
pyc = py + 'c'
|
pyc = py + 'c'
|
||||||
if os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime:
|
if py not in changed and os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime:
|
||||||
#if debug:
|
#if debug:
|
||||||
#print "Ignoring module %s; unchanged" % str(mod)
|
#print "Ignoring module %s; unchanged" % str(mod)
|
||||||
continue
|
continue
|
||||||
|
changed.append(py) ## keep track of which modules have changed to insure that duplicate-import modules get reloaded.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reload(mod, debug=debug)
|
reload(mod, debug=debug)
|
||||||
@ -73,7 +75,7 @@ def reload(module, debug=False, lists=False, dicts=False):
|
|||||||
- Requires that class and function names have not changed
|
- Requires that class and function names have not changed
|
||||||
"""
|
"""
|
||||||
if debug:
|
if debug:
|
||||||
print("Reloading", module)
|
print("Reloading %s" % str(module))
|
||||||
|
|
||||||
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
|
## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison
|
||||||
oldDict = module.__dict__.copy()
|
oldDict = module.__dict__.copy()
|
||||||
@ -158,7 +160,7 @@ def updateClass(old, new, debug):
|
|||||||
if isinstance(ref, old) and ref.__class__ is old:
|
if isinstance(ref, old) and ref.__class__ is old:
|
||||||
ref.__class__ = new
|
ref.__class__ = new
|
||||||
if debug:
|
if debug:
|
||||||
print(" Changed class for", safeStr(ref))
|
print(" Changed class for %s" % safeStr(ref))
|
||||||
elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__:
|
elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__:
|
||||||
ind = ref.__bases__.index(old)
|
ind = ref.__bases__.index(old)
|
||||||
|
|
||||||
@ -174,7 +176,7 @@ def updateClass(old, new, debug):
|
|||||||
## (and I presume this may slow things down?)
|
## (and I presume this may slow things down?)
|
||||||
ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
|
ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:]
|
||||||
if debug:
|
if debug:
|
||||||
print(" Changed superclass for", safeStr(ref))
|
print(" Changed superclass for %s" % safeStr(ref))
|
||||||
#else:
|
#else:
|
||||||
#if debug:
|
#if debug:
|
||||||
#print " Ignoring reference", type(ref)
|
#print " Ignoring reference", type(ref)
|
||||||
@ -208,7 +210,7 @@ def updateClass(old, new, debug):
|
|||||||
for attr in dir(new):
|
for attr in dir(new):
|
||||||
if not hasattr(old, attr):
|
if not hasattr(old, attr):
|
||||||
if debug:
|
if debug:
|
||||||
print(" Adding missing attribute", attr)
|
print(" Adding missing attribute %s" % attr)
|
||||||
setattr(old, attr, getattr(new, attr))
|
setattr(old, attr, getattr(new, attr))
|
||||||
|
|
||||||
## finally, update any previous versions still hanging around..
|
## finally, update any previous versions still hanging around..
|
||||||
|
18
setup.py
18
setup.py
@ -9,7 +9,11 @@ all_packages = ['.'.join(p) for p in subdirs]
|
|||||||
setup(name='pyqtgraph',
|
setup(name='pyqtgraph',
|
||||||
version='',
|
version='',
|
||||||
description='Scientific Graphics and GUI Library for Python',
|
description='Scientific Graphics and GUI Library for Python',
|
||||||
long_description="PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.",
|
long_description="""\
|
||||||
|
PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy.
|
||||||
|
|
||||||
|
It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.
|
||||||
|
""",
|
||||||
license='MIT',
|
license='MIT',
|
||||||
url='http://www.pyqtgraph.org',
|
url='http://www.pyqtgraph.org',
|
||||||
author='Luke Campagnola',
|
author='Luke Campagnola',
|
||||||
@ -17,5 +21,17 @@ setup(name='pyqtgraph',
|
|||||||
packages=all_packages,
|
packages=all_packages,
|
||||||
package_dir = {'pyqtgraph': '.'},
|
package_dir = {'pyqtgraph': '.'},
|
||||||
package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']},
|
package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']},
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Other Environment",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Topic :: Scientific/Engineering :: Visualization",
|
||||||
|
"Topic :: Software Development :: User Interfaces",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user