From f6da6e2fd0ff22adcac8dbbb101d209dd4bb1d4b Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 22:13:41 -0400 Subject: [PATCH] Added matplotlib exporter Updates to MeshData class (this is still not tested) --- GraphicsScene/exportDialog.py | 7 +- __init__.py | 6 +- exporters/Matplotlib.py | 74 ++++++++++++ functions.py | 179 ++++++++--------------------- graphicsItems/PlotDataItem.py | 16 ++- graphicsItems/PlotItem/PlotItem.py | 12 +- opengl/MeshData.py | 111 +++++++++++++++++- widgets/MatplotlibWidget.py | 37 ++++++ 8 files changed, 292 insertions(+), 150 deletions(-) create mode 100644 exporters/Matplotlib.py create mode 100644 widgets/MatplotlibWidget.py diff --git a/GraphicsScene/exportDialog.py b/GraphicsScene/exportDialog.py index f9ed5763..72809d44 100644 --- a/GraphicsScene/exportDialog.py +++ b/GraphicsScene/exportDialog.py @@ -72,6 +72,8 @@ class ExportDialog(QtGui.QWidget): def exportItemChanged(self, item, prev): + if item is None: + return if item.gitem is self.scene: newBounds = self.scene.views()[0].viewRect() else: @@ -105,7 +107,10 @@ class ExportDialog(QtGui.QWidget): expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) params = exp.parameters() - self.ui.paramTree.setParameters(params) + if params is None: + self.ui.paramTree.clear() + else: + self.ui.paramTree.setParameters(params) self.currentExporter = exp def exportClicked(self): diff --git a/__init__.py b/__init__.py index 360e217f..4256c0e3 100644 --- a/__init__.py +++ b/__init__.py @@ -57,7 +57,7 @@ renamePyc(path) ## don't import the more complex systems--canvas, parametertree, flowchart, dockarea ## these must be imported separately. -def importAll(path): +def importAll(path, excludes=()): d = os.path.join(os.path.split(__file__)[0], path) files = [] for f in os.listdir(d): @@ -67,6 +67,8 @@ def importAll(path): files.append(f[:-3]) for modName in files: + if modName in excludes: + continue mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) if hasattr(mod, '__all__'): names = mod.__all__ @@ -77,7 +79,7 @@ def importAll(path): globals()[k] = getattr(mod, k) importAll('graphicsItems') -importAll('widgets') +importAll('widgets', excludes=['MatplotlibWidget']) from imageview import * from WidgetGroup import * diff --git a/exporters/Matplotlib.py b/exporters/Matplotlib.py new file mode 100644 index 00000000..71164b8e --- /dev/null +++ b/exporters/Matplotlib.py @@ -0,0 +1,74 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from Exporter import Exporter + + +__all__ = ['MatplotlibExporter'] + + +class MatplotlibExporter(Exporter): + Name = "Matplotlib Window" + windows = [] + def __init__(self, item): + Exporter.__init__(self, item) + + def parameters(self): + return None + + def export(self, fileName=None): + + if isinstance(self.item, pg.PlotItem): + mpw = MatplotlibWindow() + MatplotlibExporter.windows.append(mpw) + fig = mpw.getFigure() + + ax = fig.add_subplot(111) + ax.clear() + #ax.grid(True) + + for item in self.item.curves: + x, y = item.getData() + opts = item.opts + pen = pg.mkPen(opts['pen']) + if pen.style() == QtCore.Qt.NoPen: + linestyle = '' + else: + linestyle = '-' + color = tuple([c/255. for c in pg.colorTuple(pen.color())]) + symbol = opts['symbol'] + if symbol == 't': + symbol = '^' + symbolPen = pg.mkPen(opts['symbolPen']) + symbolBrush = pg.mkBrush(opts['symbolBrush']) + markeredgecolor = tuple([c/255. for c in pg.colorTuple(symbolPen.color())]) + markerfacecolor = tuple([c/255. for c in pg.colorTuple(symbolBrush.color())]) + + if opts['fillLevel'] is not None and opts['fillBrush'] is not None: + fillBrush = pg.mkBrush(opts['fillBrush']) + fillcolor = tuple([c/255. for c in pg.colorTuple(fillBrush.color())]) + ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) + + ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor) + + xr, yr = self.item.viewRange() + ax.set_xbound(*xr) + ax.set_ybound(*yr) + mpw.draw() + else: + raise Exception("Matplotlib export currently only works with plot items") + + + +class MatplotlibWindow(QtGui.QMainWindow): + def __init__(self): + import pyqtgraph.widgets.MatplotlibWidget + QtGui.QMainWindow.__init__(self) + self.mpl = pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget() + self.setCentralWidget(self.mpl) + self.show() + + def __getattr__(self, attr): + return getattr(self.mpl, attr) + + def closeEvent(self, ev): + MatplotlibExporter.windows.remove(self) diff --git a/functions.py b/functions.py index 624e90b4..7a582c4a 100644 --- a/functions.py +++ b/functions.py @@ -409,12 +409,12 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): -def makeARGB(data, lut=None, levels=None, useRGBA=False): +def makeARGB(data, lut=None, levels=None): """ Convert a 2D or 3D array into an ARGB array suitable for building QImages Will optionally do scaling and/or table lookups to determine final colors. - Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. + Returns the ARGB array and a boolean indicating whether there is alpha channel data. Arguments: data - 2D or 3D numpy array of int/float types @@ -433,8 +433,6 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): Lookup tables can be built using GradientWidget. levels - List [min, max]; optionally rescale data before converting through the lookup table. rescaled = (data-min) * len(lut) / (max-min) - useRGBA - If True, the data is returned in RGBA order. The default is - False, which returns in BGRA order for use with QImage. """ @@ -582,11 +580,8 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): prof.mark('4') - if useRGBA: - order = [0,1,2,3] ## array comes out RGBA - else: - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - + + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. if data.shape[2] == 1: for i in xrange(3): imgData[..., order[i]] = data[..., 0] @@ -737,85 +732,7 @@ def rescaleData(data, scale, offset): #return facets -def isocurve(data, level): - """ - 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 - - This function is SLOW; plenty of room for optimization here. - """ - sideTable = [ - [], - [0,1], - [1,2], - [0,2], - [0,3], - [1,3], - [0,1,2,3], - [2,3], - [2,3], - [0,1,2,3], - [1,3], - [0,3], - [0,2], - [1,2], - [0,1], - [] - ] - - edgeKey=[ - [(0,1),(0,0)], - [(0,0), (1,0)], - [(1,0), (1,1)], - [(1,1), (0,1)] - ] - - - lines = [] - - ## mark everything below the isosurface level - mask = data < level - - ### make four sub-fields and compute indexes for grid cells - index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) - fields = np.empty((2,2), dtype=object) - slices = [slice(0,-1), slice(1,None)] - for i in [0,1]: - for j in [0,1]: - fields[i,j] = mask[slices[i], slices[j]] - #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme - vertIndex = i+2*j - #print i,j,k," : ", fields[i,j,k], 2**vertIndex - index += fields[i,j] * 2**vertIndex - #print index - #print index - - ## add lines - for i in xrange(index.shape[0]): # data x-axis - for j in xrange(index.shape[1]): # data y-axis - sides = sideTable[index[i,j]] - for l in range(0, len(sides), 2): ## faces for this grid cell - edges = sides[l:l+2] - pts = [] - for m in [0,1]: # points in this face - p1 = edgeKey[edges[m]][0] # p1, p2 are points at either side of an edge - p2 = edgeKey[edges[m]][1] - v1 = data[i+p1[0], j+p1[1]] # v1 and v2 are the values at p1 and p2 - v2 = data[i+p2[0], j+p2[1]] - 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 - ) - pts.append(p) - lines.append(pts) - - return lines ## a list of pairs of points - def isosurface(data, level): """ @@ -1193,55 +1110,55 @@ def isosurface(data, level): return facets +## code has moved to opengl/MeshData.py +#def meshNormals(data): + #""" + #Return list of normal vectors and list of faces which reference the normals + #data must be list of triangles; each triangle is a list of three points + #[ [(x,y,z), (x,y,z), (x,y,z)], ...] + #Return values are + #normals: [(x,y,z), ...] + #faces: [(n1, n2, n3), ...] + #""" -def meshNormals(data): - """ - Return list of normal vectors and list of faces which reference the normals - data must be list of triangles; each triangle is a list of three points - [ [(x,y,z), (x,y,z), (x,y,z)], ...] - Return values are - normals: [(x,y,z), ...] - faces: [(n1, n2, n3), ...] - """ - - normals = [] - points = {} - for i, face in enumerate(data): - ## compute face normal - pts = [QtGui.QVector3D(*x) for x in face] - norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) - normals.append(norm) + #normals = [] + #points = {} + #for i, face in enumerate(data): + ### compute face normal + #pts = [QtGui.QVector3D(*x) for x in face] + #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + #normals.append(norm) - ## remember each point was associated with this normal - for p in face: - p = tuple(map(lambda x: np.round(x, 8), p)) - if p not in points: - points[p] = [] - points[p].append(i) + ### remember each point was associated with this normal + #for p in face: + #p = tuple(map(lambda x: np.round(x, 8), p)) + #if p not in points: + #points[p] = [] + #points[p].append(i) - ## compute averages - avgLookup = {} - avgNorms = [] - for k,v in points.iteritems(): - norms = [normals[i] for i in v] - a = norms[0] - if len(v) > 1: - for n in norms[1:]: - a = a + n - a = a / len(v) - avgLookup[k] = len(avgNorms) - avgNorms.append(a) + ### compute averages + #avgLookup = {} + #avgNorms = [] + #for k,v in points.iteritems(): + #norms = [normals[i] for i in v] + #a = norms[0] + #if len(v) > 1: + #for n in norms[1:]: + #a = a + n + #a = a / len(v) + #avgLookup[k] = len(avgNorms) + #avgNorms.append(a) - ## generate return array - faces = [] - for i, face in enumerate(data): - f = [] - for p in face: - p = tuple(map(lambda x: np.round(x, 8), p)) - f.append(avgLookup[p]) - faces.append(tuple(f)) + ### generate return array + #faces = [] + #for i, face in enumerate(data): + #f = [] + #for p in face: + #p = tuple(map(lambda x: np.round(x, 8), p)) + #f.append(avgLookup[p]) + #faces.append(tuple(f)) - return avgNorms, faces + #return avgNorms, faces diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index a8a46c4f..1938cd50 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -98,7 +98,7 @@ class PlotDataItem(GraphicsObject): 'pen': (200,200,200), 'shadowPen': None, 'fillLevel': None, - 'brush': None, + 'fillBrush': None, 'symbol': None, 'symbolSize': 10, @@ -165,10 +165,13 @@ class PlotDataItem(GraphicsObject): #self.update() self.updateItems() - def setBrush(self, *args, **kargs): + def setFillBrush(self, *args, **kargs): brush = fn.mkBrush(*args, **kargs) - self.opts['brush'] = brush + self.opts['fillBrush'] = brush self.updateItems() + + def setBrush(self, *args, **kargs): + return self.setFillBrush(*args, **kargs) def setFillLevel(self, level): self.opts['fillLevel'] = level @@ -268,6 +271,9 @@ class PlotDataItem(GraphicsObject): if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): kargs['symbol'] = 'o' + if 'brush' in kargs: + kargs['fillBrush'] = kargs['brush'] + for k in self.opts.keys(): if k in kargs: self.opts[k] = kargs[k] @@ -313,8 +319,8 @@ class PlotDataItem(GraphicsObject): #c.scene().removeItem(c) curveArgs = {} - for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: - curveArgs[k] = self.opts[k] + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush')]: + curveArgs[v] = self.opts[k] scatterArgs = {} for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]: diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index cef568c5..333516dc 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -146,11 +146,11 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox - for m in [ - 'setXRange', 'setYRange', 'setXLink', 'setYLink', - 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', - 'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: - setattr(self, m, getattr(self.vb, m)) + #for m in [ + #'setXRange', 'setYRange', 'setXLink', 'setYLink', + #'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', + #'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: + #setattr(self, m, getattr(self.vb, m)) self.items = [] self.curves = [] @@ -296,6 +296,8 @@ class PlotItem(GraphicsWidget): #QtGui.QGraphicsWidget.paint(self, *args) #prof.finish() + def __getattr__(self, attr): ## wrap ms + return getattr(self.vb, attr) def close(self): #print "delete", self diff --git a/opengl/MeshData.py b/opengl/MeshData.py index f6d0ae7c..15139bc1 100644 --- a/opengl/MeshData.py +++ b/opengl/MeshData.py @@ -8,18 +8,117 @@ class MeshData(object): - normals per vertex or tri """ - def __init__(self ...): - - - def generateFaceNormals(self): + def __init__(self): + self.vertexes = [] + self.edges = None + self.faces = [] + self.vertexFaces = None ## maps vertex ID to a list of face IDs + self.vertexNormals = None + self.faceNormals = None + self.vertexColors = None + self.edgeColors = None + self.faceColors = None + def setFaces(self, faces, vertexes=None): + """ + Set the faces in this data set. + Data may be provided either as an Nx3x3 list of floats (9 float coordinate values per face) + *faces* = [ [(x, y, z), (x, y, z), (x, y, z)], ... ] + or as an Nx3 list of ints (vertex integers) AND an Mx3 list of floats (3 float coordinate values per vertex) + *faces* = [ (p1, p2, p3), ... ] + *vertexes* = [ (x, y, z), ... ] + """ + + if vertexes is None: + self._setUnindexedFaces(self, faces) + else: + self._setIndexedFaces(self, faces) + + def _setUnindexedFaces(self, faces): + verts = {} + self.faces = [] + self.vertexes = [] + self.vertexFaces = [] + self.faceNormals = None + self.vertexNormals = None + for face in faces: + inds = [] + for pt in face: + pt2 = tuple([int(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged + index = verts.get(pt2, None) + if index is None: + self.vertexes.append(tuple(pt)) + self.vertexFaces.append([]) + index = len(self.vertexes)-1 + verts[pt2] = index + self.vertexFaces[index].append(face) + inds.append(index) + self.faces.append(tuple(inds)) - def generateVertexNormals(self): + def _setIndexedFaces(self, faces, vertexes): + self.vertexes = vertexes + self.faces = faces + self.edges = None + self.vertexFaces = None + self.faceNormals = None + self.vertexNormals = None + + def getVertexFaces(self): + """ + Return list mapping each vertex index to a list of face indexes that use the vertex. + """ + if self.vertexFaces is None: + self.vertexFaces = [[]] * len(self.vertexes) + for i, face in enumerate(self.faces): + for ind in face: + if len(self.vertexFaces[ind]) == 0: + self.vertexFaces[ind] = [] ## need a unique/empty list to fill + self.vertexFaces[ind].append(i) + return self.vertexFaces + + + def getFaceNormals(self): + """ + Computes and stores normal of each face. + """ + if self.faceNormals is None: + self.faceNormals = [] + for i, face in enumerate(self.faces): + ## compute face normal + pts = [QtGui.QVector3D(*self.vertexes[vind]) for vind in face] + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + self.faceNormals.append(norm) + return self.faceNormals + + def getVertexNormals(self): """ Assigns each vertex the average of its connected face normals. If face normals have not been computed yet, then generateFaceNormals will be called. """ + if self.vertexNormals is None: + faceNorms = self.getFaceNormals() + vertFaces = self.getVertexFaces() + self.vertexNormals = [] + for vindex in xrange(len(self.vertexes)): + norms = [faceNorms[findex] for findex in vertFaces[vindex]] + if len(norms) == 0: + norm = QtGui.QVector3D() + else: + norm = reduce(QtGui.QVector3D.__add__, facenorms) / float(len(norms)) + self.vertexNormals.append(norm) + return self.vertexNormals def reverseNormals(self): - \ No newline at end of file + """ + Reverses the direction of all normal vectors. + """ + pass + + def generateEdgesFromFaces(self): + """ + Generate a set of edges by listing all the edges of faces and removing any duplicates. + Useful for displaying wireframe meshes. + """ + pass + diff --git a/widgets/MatplotlibWidget.py b/widgets/MatplotlibWidget.py new file mode 100644 index 00000000..25e058f9 --- /dev/null +++ b/widgets/MatplotlibWidget.py @@ -0,0 +1,37 @@ +from pyqtgraph.Qt import QtGui, QtCore +import matplotlib +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar +from matplotlib.figure import Figure + +class MatplotlibWidget(QtGui.QWidget): + """ + Implements a Matplotlib figure inside a QWidget. + Use getFigure() and redraw() to interact with matplotlib. + + Example:: + + mw = MatplotlibWidget() + subplot = mw.getFigure().add_subplot(111) + subplot.plot(x,y) + mw.draw() + """ + + def __init__(self, size=(5.0, 4.0), dpi=100): + QtGui.QWidget.__init__(self) + self.fig = Figure(size, dpi=dpi) + self.canvas = FigureCanvas(self.fig) + self.canvas.setParent(self) + self.toolbar = NavigationToolbar(self.canvas, self) + + self.vbox = QtGui.QVBoxLayout() + self.vbox.addWidget(self.toolbar) + self.vbox.addWidget(self.canvas) + + self.setLayout(self.vbox) + + def getFigure(self): + return self.fig + + def draw(self): + self.canvas.draw()