merge from Luke

This commit is contained in:
Megan Kratz 2012-11-27 14:56:02 -05:00
commit 70fde35e3b
29 changed files with 1956 additions and 446 deletions

72
examples/GLIsosurface.py Normal file
View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
## This example uses the isosurface function to convert a scalar field
## (a hydrogen orbital) into a mesh for 3D display.
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
w.setCameraPosition(distance=40)
g = gl.GLGridItem()
g.scale(2,2,1)
w.addItem(g)
import numpy as np
## Define a scalar field from which we will generate an isosurface
def psi(i, j, k, offset=(25, 25, 50)):
x = i-offset[0]
y = j-offset[1]
z = k-offset[2]
th = np.arctan2(z, (x**2+y**2)**0.5)
phi = np.arctan2(y, x)
r = (x**2 + y**2 + z **2)**0.5
a0 = 1
#ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th)
ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1)
return ps
#return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2
print("Generating scalar field..")
data = np.abs(np.fromfunction(psi, (50,50,100)))
print("Generating isosurface..")
verts = pg.isosurface(data, data.max()/4.)
md = gl.MeshData.MeshData(vertexes=verts)
colors = np.ones((md.faceCount(), 4), dtype=float)
colors[:,3] = 0.2
colors[:,2] = np.linspace(0, 1, colors.shape[0])
md.setFaceColors(colors)
m1 = gl.GLMeshItem(meshdata=md, smooth=False, shader='balloon')
m1.setGLOptions('additive')
#w.addItem(m1)
m1.translate(-25, -25, -20)
m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='balloon')
m2.setGLOptions('additive')
w.addItem(m2)
m2.translate(-25, -25, -50)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()

View File

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Simple examples demonstrating the use of GLMeshItem.
## This example uses the isosurface function to convert a scalar field """
## (a hydrogen orbital) into a mesh for 3D display.
## Add path to library (just for examples; you do not need this) ## Add path to library (just for examples; you do not need this)
import sys, os import sys, os
@ -15,52 +16,117 @@ app = QtGui.QApplication([])
w = gl.GLViewWidget() w = gl.GLViewWidget()
w.show() w.show()
w.setCameraPosition(distance=40)
g = gl.GLGridItem() g = gl.GLGridItem()
g.scale(2,2,1) g.scale(2,2,1)
w.addItem(g) w.addItem(g)
import numpy as np import numpy as np
def psi(i, j, k, offset=(25, 25, 50)):
x = i-offset[0] ## Example 1:
y = j-offset[1] ## Array of vertex positions and array of vertex indexes defining faces
z = k-offset[2] ## Colors are specified per-face
th = np.arctan2(z, (x**2+y**2)**0.5)
phi = np.arctan2(y, x) verts = np.array([
r = (x**2 + y**2 + z **2)**0.5 [0, 0, 0],
a0 = 1 [2, 0, 0],
#ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) [1, 2, 0],
ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) [1, 1, 1],
])
faces = np.array([
[0, 1, 2],
[0, 1, 3],
[0, 2, 3],
[1, 2, 3]
])
colors = np.array([
[1, 0, 0, 0.3],
[0, 1, 0, 0.3],
[0, 0, 1, 0.3],
[1, 1, 0, 0.3]
])
## Mesh item will automatically compute face normals.
m1 = gl.GLMeshItem(vertexes=verts, faces=faces, faceColors=colors, smooth=False)
m1.translate(5, 5, 0)
m1.setGLOptions('additive')
w.addItem(m1)
## Example 2:
## Array of vertex positions, three per face
## Colors are specified per-vertex
verts = verts[faces] ## Same mesh geometry as example 2, but now we are passing in 12 vertexes
colors = np.random.random(size=(verts.shape[0], 3, 4))
#colors[...,3] = 1.0
m2 = gl.GLMeshItem(vertexes=verts, vertexColors=colors, smooth=False, shader='balloon')
m2.translate(-5, 5, 0)
w.addItem(m2)
## Example 3:
## icosahedron
md = gl.MeshData.sphere(rows=10, cols=20)
#colors = np.random.random(size=(md.faceCount(), 4))
#colors[:,3] = 0.3
#colors[100:] = 0.0
colors = np.ones((md.faceCount(), 4), dtype=float)
colors[::2,0] = 0
colors[:,1] = np.linspace(0, 1, colors.shape[0])
md.setFaceColors(colors)
m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon')
#m3.translate(-5, -5, 0)
w.addItem(m3)
#def psi(i, j, k, offset=(25, 25, 50)):
#x = i-offset[0]
#y = j-offset[1]
#z = k-offset[2]
#th = np.arctan2(z, (x**2+y**2)**0.5)
#phi = np.arctan2(y, x)
#r = (x**2 + y**2 + z **2)**0.5
#a0 = 1
##ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th)
#ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1)
return ps #return ps
#return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 ##return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2
print("Generating scalar field..") #print("Generating scalar field..")
data = np.abs(np.fromfunction(psi, (50,50,100))) #data = np.abs(np.fromfunction(psi, (50,50,100)))
#data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); ##data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50));
print("Generating isosurface..") #print("Generating isosurface..")
faces = pg.isosurface(data, data.max()/4.) #verts = pg.isosurface(data, data.max()/4.)
m = gl.GLMeshItem(faces)
w.addItem(m) #md = gl.MeshData.MeshData(vertexes=verts)
m.translate(-25, -25, -50)
#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float)
#colors[:,3] = 0.3
#colors[:,2] = np.linspace(0, 1, colors.shape[0])
#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False)
#w.addItem(m1)
#m1.translate(-25, -25, -20)
#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True)
#w.addItem(m2)
#m2.translate(-25, -25, -50)
#data = np.zeros((5,5,5))
#data[2,2,1:4] = 1
#data[2,1:4,2] = 1
#data[1:4,2,2] = 1
#tr.translate(-2.5, -2.5, 0)
#data = np.ones((2,2,2))
#data[0, 1, 0] = 0
#faces = pg.isosurface(data, 0.5)
#m = gl.GLMeshItem(faces)
#w.addItem(m)
#m.setTransform(tr)
## Start Qt event loop unless running in interactive mode. ## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1: if sys.flags.interactive != 1:

98
examples/GLSurfacePlot.py Normal file
View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of GLSurfacePlotItem.
"""
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
import scipy.ndimage as ndi
import numpy as np
## Create a GL View widget to display data
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
w.setCameraPosition(distance=50)
## Add a grid to the view
g = gl.GLGridItem()
g.scale(2,2,1)
g.setDepthValue(10) # draw grid after surfaces since they may be translucent
w.addItem(g)
## Simple surface plot example
## x, y values are not specified, so assumed to be 0:50
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
p1 = gl.GLSurfacePlotItem(z=z, shader='shaded', color=(0.5, 0.5, 1, 1))
p1.scale(16./49., 16./49., 1.0)
p1.translate(-18, 2, 0)
w.addItem(p1)
## Saddle example with x and y specified
x = np.linspace(-8, 8, 50)
y = np.linspace(-8, 8, 50)
z = 0.1 * ((x.reshape(50,1) ** 2) - (y.reshape(1,50) ** 2))
p2 = gl.GLSurfacePlotItem(x=x, y=y, z=z, shader='normalColor')
p2.translate(-10,-10,0)
w.addItem(p2)
## Manually specified colors
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
x = np.linspace(-12, 12, 50)
y = np.linspace(-12, 12, 50)
colors = np.ones((50,50,4), dtype=float)
colors[...,0] = np.clip(np.cos(((x.reshape(50,1) ** 2) + (y.reshape(1,50) ** 2)) ** 0.5), 0, 1)
colors[...,1] = colors[...,0]
p3 = gl.GLSurfacePlotItem(z=z, colors=colors.reshape(50*50,4), shader='shaded', smooth=False)
p3.scale(16./49., 16./49., 1.0)
p3.translate(2, -18, 0)
w.addItem(p3)
## Animated example
## compute surface vertex data
cols = 100
rows = 100
x = np.linspace(-8, 8, cols+1).reshape(cols+1,1)
y = np.linspace(-8, 8, rows+1).reshape(1,rows+1)
d = (x**2 + y**2) * 0.1
d2 = d ** 0.5 + 0.1
## precompute height values for all frames
phi = np.arange(0, np.pi*2, np.pi/20.)
z = np.sin(d[np.newaxis,...] + phi.reshape(phi.shape[0], 1, 1)) / d2[np.newaxis,...]
## create a surface plot, tell it to use the 'heightColor' shader
## since this does not require normal vectors to render (thus we
## can set computeNormals=False to save time when the mesh updates)
p4 = gl.GLSurfacePlotItem(x=x[:,0], y = y[0,:], shader='heightColor', computeNormals=False, smooth=False)
p4.shader()['colorMap'] = np.array([0.2, 2, 0.5, 0.2, 1, 1, 0.2, 0, 2])
p4.translate(10, 10, 0)
w.addItem(p4)
index = 0
def update():
global p4, z, index
index -= 1
p4.setData(z=z[index%z.shape[0]])
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(30)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()

108
examples/GLshaders.py Normal file
View File

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
"""
Demonstration of some of the shader programs included with pyqtgraph.
"""
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
w.setCameraPosition(distance=15, azimuth=-90)
g = gl.GLGridItem()
g.scale(2,2,1)
w.addItem(g)
import numpy as np
md = gl.MeshData.sphere(rows=10, cols=20)
x = np.linspace(-8, 8, 6)
m1 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 0.2), shader='balloon', glOptions='additive')
m1.translate(x[0], 0, 0)
m1.scale(1, 1, 2)
w.addItem(m1)
m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='normalColor', glOptions='opaque')
m2.translate(x[1], 0, 0)
m2.scale(1, 1, 2)
w.addItem(m2)
m3 = gl.GLMeshItem(meshdata=md, smooth=True, shader='viewNormalColor', glOptions='opaque')
m3.translate(x[2], 0, 0)
m3.scale(1, 1, 2)
w.addItem(m3)
m4 = gl.GLMeshItem(meshdata=md, smooth=True, shader='shaded', glOptions='opaque')
m4.translate(x[3], 0, 0)
m4.scale(1, 1, 2)
w.addItem(m4)
m5 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='edgeHilight', glOptions='opaque')
m5.translate(x[4], 0, 0)
m5.scale(1, 1, 2)
w.addItem(m5)
m6 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='heightColor', glOptions='opaque')
m6.translate(x[5], 0, 0)
m6.scale(1, 1, 2)
w.addItem(m6)
#def psi(i, j, k, offset=(25, 25, 50)):
#x = i-offset[0]
#y = j-offset[1]
#z = k-offset[2]
#th = np.arctan2(z, (x**2+y**2)**0.5)
#phi = np.arctan2(y, x)
#r = (x**2 + y**2 + z **2)**0.5
#a0 = 1
##ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th)
#ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1)
#return ps
##return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2
#print("Generating scalar field..")
#data = np.abs(np.fromfunction(psi, (50,50,100)))
##data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50));
#print("Generating isosurface..")
#verts = pg.isosurface(data, data.max()/4.)
#md = gl.MeshData.MeshData(vertexes=verts)
#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float)
#colors[:,3] = 0.3
#colors[:,2] = np.linspace(0, 1, colors.shape[0])
#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False)
#w.addItem(m1)
#m1.translate(-25, -25, -20)
#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True)
#w.addItem(m2)
#m2.translate(-25, -25, -50)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()

View File

@ -6,13 +6,14 @@ from pyqtgraph.Qt import QtCore, QtGui
plt = pg.plot() plt = pg.plot()
l = pg.LegendItem((100,60), (60,10)) # args are (size, position) plt.addLegend()
l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case #l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset)
#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case
c1 = plt.plot([1,3,2,4], pen='r') c1 = plt.plot([1,3,2,4], pen='r', name='red plot')
c2 = plt.plot([2,1,4,3], pen='g') c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot')
l.addItem(c1, 'red plot') #l.addItem(c1, 'red plot')
l.addItem(c2, 'green plot') #l.addItem(c2, 'green plot')
## Start Qt event loop unless running in interactive mode or using pyside. ## Start Qt event loop unless running in interactive mode or using pyside.

View File

@ -61,37 +61,41 @@ ui.alphaCheck.toggled.connect(updateLUT)
def updateScale(): def updateScale():
global ui global ui
spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3] spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3]
if ui.rgbCheck.isChecked(): if ui.rgbLevelsCheck.isChecked():
for s in spins[2:]: for s in spins[2:]:
s.setEnabled(True) s.setEnabled(True)
else: else:
for s in spins[2:]: for s in spins[2:]:
s.setEnabled(False) s.setEnabled(False)
ui.rgbCheck.toggled.connect(updateScale) ui.rgbLevelsCheck.toggled.connect(updateScale)
cache = {} cache = {}
def mkData(): def mkData():
global data, cache, ui global data, cache, ui
dtype = ui.dtypeCombo.currentText() dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked())
if dtype not in cache: if dtype not in cache:
if dtype == 'uint8': if dtype[0] == 'uint8':
dt = np.uint8 dt = np.uint8
loc = 128 loc = 128
scale = 64 scale = 64
mx = 255 mx = 255
elif dtype == 'uint16': elif dtype[0] == 'uint16':
dt = np.uint16 dt = np.uint16
loc = 4096 loc = 4096
scale = 1024 scale = 1024
mx = 2**16 mx = 2**16
elif dtype == 'float': elif dtype[0] == 'float':
dt = np.float dt = np.float
loc = 1.0 loc = 1.0
scale = 0.1 scale = 0.1
data = np.random.normal(size=(20,512,512), loc=loc, scale=scale) if ui.rgbCheck.isChecked():
data = ndi.gaussian_filter(data, (0, 3, 3)) data = np.random.normal(size=(20,512,512,3), loc=loc, scale=scale)
if dtype != 'float': data = ndi.gaussian_filter(data, (0, 6, 6, 0))
else:
data = np.random.normal(size=(20,512,512), loc=loc, scale=scale)
data = ndi.gaussian_filter(data, (0, 6, 6))
if dtype[0] != 'float':
data = np.clip(data, 0, mx) data = np.clip(data, 0, mx)
data = data.astype(dt) data = data.astype(dt)
cache[dtype] = data cache[dtype] = data
@ -100,7 +104,7 @@ def mkData():
updateLUT() updateLUT()
mkData() mkData()
ui.dtypeCombo.currentIndexChanged.connect(mkData) ui.dtypeCombo.currentIndexChanged.connect(mkData)
ui.rgbCheck.toggled.connect(mkData)
ptr = 0 ptr = 0
lastTime = ptime.time() lastTime = ptime.time()
@ -113,7 +117,7 @@ def update():
useLut = None useLut = None
if ui.scaleCheck.isChecked(): if ui.scaleCheck.isChecked():
if ui.rgbCheck.isChecked(): if ui.rgbLevelsCheck.isChecked():
useScale = [ useScale = [
[ui.minSpin1.value(), ui.maxSpin1.value()], [ui.minSpin1.value(), ui.maxSpin1.value()],
[ui.minSpin2.value(), ui.maxSpin2.value()], [ui.minSpin2.value(), ui.maxSpin2.value()],

View File

@ -25,7 +25,6 @@
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<zorder>fpsLabel</zorder>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
@ -84,7 +83,7 @@
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QCheckBox" name="rgbCheck"> <widget class="QCheckBox" name="rgbLevelsCheck">
<property name="text"> <property name="text">
<string>RGB</string> <string>RGB</string>
</property> </property>
@ -218,6 +217,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1">
<widget class="QCheckBox" name="rgbCheck">
<property name="text">
<string>RGB</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</widget> </widget>

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/VideoTemplate.ui' # Form implementation generated from reading ui file './examples/VideoTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Sun Nov 4 18:24:20 2012
# by: PyQt4 UI code generator 4.9.1 # by: PyQt4 UI code generator 4.9.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -55,9 +55,9 @@ class Ui_MainWindow(object):
self.scaleCheck = QtGui.QCheckBox(self.centralwidget) self.scaleCheck = QtGui.QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName(_fromUtf8("scaleCheck")) self.scaleCheck.setObjectName(_fromUtf8("scaleCheck"))
self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1)
self.rgbCheck = QtGui.QCheckBox(self.centralwidget) self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName(_fromUtf8("rgbCheck")) self.rgbLevelsCheck.setObjectName(_fromUtf8("rgbLevelsCheck"))
self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1)
self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
self.minSpin1 = SpinBox(self.centralwidget) self.minSpin1 = SpinBox(self.centralwidget)
@ -124,6 +124,9 @@ class Ui_MainWindow(object):
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
self.fpsLabel.setObjectName(_fromUtf8("fpsLabel")) self.fpsLabel.setObjectName(_fromUtf8("fpsLabel"))
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName(_fromUtf8("rgbCheck"))
self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
@ -138,12 +141,13 @@ class Ui_MainWindow(object):
self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8))
self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8))
self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8))
self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8))
self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8))
self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/VideoTemplate.ui' # Form implementation generated from reading ui file './examples/VideoTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Sun Nov 4 18:24:21 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0 # by: pyside-uic 0.2.13 running on PySide 1.1.0
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -50,9 +50,9 @@ class Ui_MainWindow(object):
self.scaleCheck = QtGui.QCheckBox(self.centralwidget) self.scaleCheck = QtGui.QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName("scaleCheck") self.scaleCheck.setObjectName("scaleCheck")
self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1)
self.rgbCheck = QtGui.QCheckBox(self.centralwidget) self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName("rgbCheck") self.rgbLevelsCheck.setObjectName("rgbLevelsCheck")
self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1)
self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setObjectName("horizontalLayout")
self.minSpin1 = SpinBox(self.centralwidget) self.minSpin1 = SpinBox(self.centralwidget)
@ -119,6 +119,9 @@ class Ui_MainWindow(object):
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
self.fpsLabel.setObjectName("fpsLabel") self.fpsLabel.setObjectName("fpsLabel")
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName("rgbCheck")
self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
@ -133,12 +136,13 @@ class Ui_MainWindow(object):
self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8))
self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8))
self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8))
self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8))
self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8))
self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget

View File

@ -43,9 +43,12 @@ examples = OrderedDict([
])), ])),
('3D Graphics', OrderedDict([ ('3D Graphics', OrderedDict([
('Volumetric', 'GLVolumeItem.py'), ('Volumetric', 'GLVolumeItem.py'),
('Isosurface', 'GLMeshItem.py'), ('Isosurface', 'GLIsosurface.py'),
('Image', 'GLImageItem.py'), ('Surface Plot', 'GLSurfacePlot.py'),
('Scatter Plot', 'GLScatterPlotItem.py'), ('Scatter Plot', 'GLScatterPlotItem.py'),
('Shaders', 'GLshaders.py'),
('Mesh', 'GLMeshItem.py'),
('Image', 'GLImageItem.py'),
])), ])),
('Widgets', OrderedDict([ ('Widgets', OrderedDict([
('PlotWidget', 'PlotWidget.py'), ('PlotWidget', 'PlotWidget.py'),
@ -127,9 +130,8 @@ class ExampleLoader(QtGui.QMainWindow):
if fn is None: if fn is None:
return return
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"', *extra) os.spawnl(os.P_NOWAIT, sys.executable, '"'+sys.executable+'"', '"' + fn + '"', *extra)
else: else:
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra) os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra)

View File

@ -434,8 +434,10 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
## make sure vectors are arrays ## make sure vectors are arrays
vectors = np.array(vectors) if not isinstance(vectors, np.ndarray):
origin = np.array(origin) vectors = np.array(vectors)
if not isinstance(origin, np.ndarray):
origin = np.array(origin)
origin.shape = (len(axes),) + (1,)*len(shape) origin.shape = (len(axes),) + (1,)*len(shape)
## Build array of sample locations. ## Build array of sample locations.
@ -580,171 +582,247 @@ def solveBilinearTransform(points1, points2):
return matrix return matrix
def rescaleData(data, scale, offset, dtype=None):
"""Return data rescaled and optionally cast to a new dtype::
data => (data-offset) * scale
Uses scipy.weave (if available) to improve performance.
"""
global USE_WEAVE
if dtype is None:
dtype = data.dtype
try:
if not USE_WEAVE:
raise Exception('Weave is disabled; falling back to slower version.')
newData = np.empty((data.size,), dtype=dtype)
flat = np.ascontiguousarray(data).reshape(data.size)
size = data.size
code = """
double sc = (double)scale;
double off = (double)offset;
for( int i=0; i<size; i++ ) {
newData[i] = ((double)flat[i] - off) * sc;
}
"""
scipy.weave.inline(code, ['flat', 'newData', 'size', 'offset', 'scale'], compiler='gcc')
data = newData.reshape(data.shape)
except:
if USE_WEAVE:
debug.printExc("Error; disabling weave.")
USE_WEAVE = False
#p = np.poly1d([scale, -offset*scale])
#data = p(data).astype(dtype)
d2 = data-offset
d2 *= scale
data = d2.astype(dtype)
return data
def applyLookupTable(data, lut):
"""
Uses values in *data* as indexes to select values from *lut*.
The returned data has shape data.shape + lut.shape[1:]
Uses scipy.weave to improve performance if it is available.
Note: color gradient lookup tables can be generated using GradientWidget.
"""
global USE_WEAVE
if data.dtype.kind not in ('i', 'u'):
data = data.astype(int)
## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well.
return np.take(lut, data, axis=0, mode='clip')
### old methods:
#data = np.clip(data, 0, lut.shape[0]-1)
#try:
#if not USE_WEAVE:
#raise Exception('Weave is disabled; falling back to slower version.')
### number of values to copy for each LUT lookup
#if lut.ndim == 1:
#ncol = 1
#else:
#ncol = sum(lut.shape[1:])
### output array
#newData = np.empty((data.size, ncol), dtype=lut.dtype)
### flattened input arrays
#flatData = data.flatten()
#flatLut = lut.reshape((lut.shape[0], ncol))
#dataSize = data.size
### strides for accessing each item
#newStride = newData.strides[0] / newData.dtype.itemsize
#lutStride = flatLut.strides[0] / flatLut.dtype.itemsize
#dataStride = flatData.strides[0] / flatData.dtype.itemsize
### strides for accessing individual values within a single LUT lookup
#newColStride = newData.strides[1] / newData.dtype.itemsize
#lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize
#code = """
#for( int i=0; i<dataSize; i++ ) {
#for( int j=0; j<ncol; j++ ) {
#newData[i*newStride + j*newColStride] = flatLut[flatData[i*dataStride]*lutStride + j*lutColStride];
#}
#}
#"""
#scipy.weave.inline(code, ['flatData', 'flatLut', 'newData', 'dataSize', 'ncol', 'newStride', 'lutStride', 'dataStride', 'newColStride', 'lutColStride'])
#newData = newData.reshape(data.shape + lut.shape[1:])
##if np.any(newData != lut[data]):
##print "mismatch!"
#data = newData
#except:
#if USE_WEAVE:
#debug.printExc("Error; disabling weave.")
#USE_WEAVE = False
#data = lut[data]
#return data
def makeRGBA(*args, **kwds): def makeRGBA(*args, **kwds):
"""Equivalent to makeARGB(..., useRGBA=True)""" """Equivalent to makeARGB(..., useRGBA=True)"""
kwds['useRGBA'] = True kwds['useRGBA'] = True
return makeARGB(*args, **kwds) return makeARGB(*args, **kwds)
def makeARGB(data, lut=None, levels=None, useRGBA=False): def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
""" """
Convert a 2D or 3D array into an ARGB array suitable for building QImages Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc.
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 (values 0-255) and a boolean indicating whether there is alpha channel data.
This is a two stage process:
1) Rescale the data based on the values in the *levels* argument (min, max).
2) Determine the final output by passing the rescaled values through a lookup table.
Both stages are optional.
============ ==================================================================================
Arguments: Arguments:
data - 2D or 3D numpy array of int/float types data numpy array of int/float types. If
levels List [min, max]; optionally rescale data before converting through the
For 2D arrays (x, y): lookup table. The data is rescaled such that min->0 and max->*scale*::
* The color will be determined using a lookup table (see argument 'lut').
* If levels are given, the data is rescaled and converted to int
before using the lookup table.
For 3D arrays (x, y, rgba): rescaled = (clip(data, min, max) - min) * (*scale* / (max - min))
* The third axis must have length 3 or 4 and will be interpreted as RGBA.
* The 'lut' argument is not allowed.
lut - Lookup table for 2D data. May be 1D or 2D (N,rgba) and must have dtype=ubyte. It is also possible to use a 2D (N,2) array of values for levels. In this case,
Values in data will be converted to color by indexing directly from lut. it is assumed that each pair of min,max values in the levels array should be
Lookup tables can be built using GradientWidget. applied to a different subset of the input data (for example, the input data may
levels - List [min, max]; optionally rescale data before converting through the already have RGB values and the levels are used to independently scale each
lookup table. rescaled = (data-min) * len(lut) / (max-min) channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is scale The maximum value to which data will be rescaled before being passed through the
False, which returns in BGRA order for use with QImage. lookup table (or returned if there is no lookup table). By default this will
be set to the length of the lookup table, or 256 is no lookup table is provided.
For OpenGL color specifications (as in GLColor4f) use scale=1.0
lut Optional lookup table (array with dtype=ubyte).
Values in data will be converted to color by indexing directly from lut.
The output data shape will be input.shape + lut.shape[1:].
Note: the output of makeARGB will have the same dtype as the lookup table, so
for conversion to QImage, the dtype must be ubyte.
Lookup tables can be built using GradientWidget.
useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
The default is False, which returns in ARGB order for use with QImage
(Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order
is BGRA).
============ ==================================================================================
""" """
prof = debug.Profiler('functions.makeARGB', disabled=True) prof = debug.Profiler('functions.makeARGB', disabled=True)
if lut is not None and not isinstance(lut, np.ndarray):
lut = np.array(lut)
if levels is not None and not isinstance(levels, np.ndarray):
levels = np.array(levels)
## sanity checks ## sanity checks
if data.ndim == 3: #if data.ndim == 3:
if data.shape[2] not in (3,4): #if data.shape[2] not in (3,4):
raise Exception("data.shape[2] must be 3 or 4") #raise Exception("data.shape[2] must be 3 or 4")
#if lut is not None: ##if lut is not None:
#raise Exception("can not use lookup table with 3D data") ##raise Exception("can not use lookup table with 3D data")
elif data.ndim != 2: #elif data.ndim != 2:
raise Exception("data must be 2D or 3D") #raise Exception("data must be 2D or 3D")
if lut is not None: #if lut is not None:
if lut.ndim == 2: ##if lut.ndim == 2:
if lut.shape[1] not in (3,4): ##if lut.shape[1] :
raise Exception("lut.shape[1] must be 3 or 4") ##raise Exception("lut.shape[1] must be 3 or 4")
elif lut.ndim != 1: ##elif lut.ndim != 1:
raise Exception("lut must be 1D or 2D") ##raise Exception("lut must be 1D or 2D")
if lut.dtype != np.ubyte: #if lut.dtype != np.ubyte:
raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype))
if levels is not None: if levels is not None:
levels = np.array(levels) if levels.ndim == 1:
if levels.shape == (2,): if len(levels) != 2:
pass raise Exception('levels argument must have length 2')
elif levels.shape in [(3,2), (4,2)]: elif levels.ndim == 2:
if data.ndim == 3: if lut is not None and lut.ndim > 1:
raise Exception("Can not use 2D levels with 3D data.") raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2')
if lut is not None: if levels.shape != (data.shape[-1], 2):
raise Exception('Can not use 2D levels and lookup table together.') raise Exception('levels must have shape (data.shape[-1], 2)')
else: else:
raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") print levels
raise Exception("levels argument must be 1D or 2D.")
#levels = np.array(levels)
#if levels.shape == (2,):
#pass
#elif levels.shape in [(3,2), (4,2)]:
#if data.ndim == 3:
#raise Exception("Can not use 2D levels with 3D data.")
#if lut is not None:
#raise Exception('Can not use 2D levels and lookup table together.')
#else:
#raise Exception("Levels must have shape (2,) or (3,2) or (4,2)")
prof.mark('1') prof.mark('1')
if lut is not None: if scale is None:
lutLength = lut.shape[0] if lut is not None:
else: scale = lut.shape[0]
lutLength = 256 else:
scale = 255.
## weave requires contiguous arrays
global USE_WEAVE
if (levels is not None or lut is not None) and USE_WEAVE:
data = np.ascontiguousarray(data)
## Apply levels if given ## Apply levels if given
if levels is not None: if levels is not None:
try: ## use weave to speed up scaling if isinstance(levels, np.ndarray) and levels.ndim == 2:
if not USE_WEAVE: ## we are going to rescale each channel independently
raise Exception('Weave is disabled; falling back to slower version.') if levels.shape[0] != data.shape[-1]:
if levels.ndim == 1: raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])")
scale = float(lutLength) / (levels[1]-levels[0]) newData = np.empty(data.shape, dtype=int)
offset = float(levels[0]) for i in range(data.shape[-1]):
data = rescaleData(data, scale, offset) minVal, maxVal = levels[i]
else: if minVal == maxVal:
if data.ndim == 2: maxVal += 1e-16
newData = np.empty(data.shape+(levels.shape[0],), dtype=np.uint32) newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int)
for i in range(levels.shape[0]): data = newData
scale = float(lutLength / (levels[i,1]-levels[i,0])) else:
offset = float(levels[i,0]) minVal, maxVal = levels
newData[...,i] = rescaleData(data, scale, offset) if minVal == maxVal:
elif data.ndim == 3: maxVal += 1e-16
newData = np.empty(data.shape, dtype=np.uint32) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
for i in range(data.shape[2]):
scale = float(lutLength / (levels[i,1]-levels[i,0]))
offset = float(levels[i,0])
#print scale, offset, data.shape, newData.shape, levels.shape
newData[...,i] = rescaleData(data[...,i], scale, offset)
data = newData
except:
if USE_WEAVE:
debug.printExc("Error; disabling weave.")
USE_WEAVE = False
if levels.ndim == 1:
if data.ndim == 2:
levels = levels[np.newaxis, np.newaxis, :]
else:
levels = levels[np.newaxis, np.newaxis, np.newaxis, :]
else:
levels = levels[np.newaxis, np.newaxis, ...]
if data.ndim == 2:
data = data[..., np.newaxis]
data = ((data-levels[...,0]) * lutLength) / (levels[...,1]-levels[...,0])
prof.mark('2') prof.mark('2')
## apply LUT if given ## apply LUT if given
if lut is not None and data.ndim == 2: if lut is not None:
data = applyLookupTable(data, lut)
if data.dtype.kind not in ('i', 'u'):
data = data.astype(int)
data = np.clip(data, 0, lutLength-1)
try:
if not USE_WEAVE:
raise Exception('Weave is disabled; falling back to slower version.')
newData = np.empty((data.size,) + lut.shape[1:], dtype=np.uint8)
flat = data.reshape(data.size)
size = data.size
ncol = lut.shape[1]
newStride = newData.strides[0]
newColStride = newData.strides[1]
lutStride = lut.strides[0]
lutColStride = lut.strides[1]
flatStride = flat.strides[0] / flat.dtype.itemsize
#print "newData:", newData.shape, newData.dtype
#print "flat:", flat.shape, flat.dtype, flat.min(), flat.max()
#print "lut:", lut.shape, lut.dtype
#print "size:", size, "ncols:", ncol
#print "strides:", newStride, newColStride, lutStride, lutColStride, flatStride
code = """
for( int i=0; i<size; i++ ) {
for( int j=0; j<ncol; j++ ) {
newData[i*newStride + j*newColStride] = lut[flat[i*flatStride]*lutStride + j*lutColStride];
}
}
"""
scipy.weave.inline(code, ['flat', 'lut', 'newData', 'size', 'ncol', 'newStride', 'lutStride', 'flatStride', 'newColStride', 'lutColStride'])
data = newData.reshape(data.shape + lut.shape[1:])
except:
if USE_WEAVE:
debug.printExc("Error; disabling weave.")
USE_WEAVE = False
data = lut[data]
else: else:
if data.dtype is not np.ubyte: if data.dtype is not np.ubyte:
data = np.clip(data, 0, 255).astype(np.ubyte) data = np.clip(data, 0, 255).astype(np.ubyte)
@ -895,25 +973,6 @@ def imageToArray(img, copy=False, transpose=True):
return arr return arr
def rescaleData(data, scale, offset):
newData = np.empty((data.size,), dtype=np.int)
flat = data.reshape(data.size)
size = data.size
code = """
double sc = (double)scale;
double off = (double)offset;
for( int i=0; i<size; i++ ) {
newData[i] = (int)(((double)flat[i] - off) * sc);
}
"""
scipy.weave.inline(code, ['flat', 'newData', 'size', 'offset', 'scale'], compiler='gcc')
data = newData.reshape(data.shape)
return data
#def isosurface(data, level): #def isosurface(data, level):
#""" #"""
#Generate isosurface from volumetric data using marching tetrahedra algorithm. #Generate isosurface from volumetric data using marching tetrahedra algorithm.
@ -1090,7 +1149,7 @@ def isosurface(data, level):
*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 a list of faces; each face is a list of three vertexes and each vertex is a tuple of three floats. Returns an array of vertex coordinates (N, 3, 3);
This function is SLOW; plenty of room for optimization here. This function is SLOW; plenty of room for optimization here.
""" """
@ -1457,7 +1516,7 @@ def isosurface(data, level):
pts.append(p) pts.append(p)
facets.append(pts) facets.append(pts)
return facets return np.array(facets)

View File

@ -166,13 +166,15 @@ class GraphicsItem(object):
## attempt to re-scale direction vector to fit within the precision of the coordinate system ## attempt to re-scale direction vector to fit within the precision of the coordinate system
if direction.x() == 0: if direction.x() == 0:
r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))
#r = 1.0/(abs(dt.m12()) + abs(dt.m22()))
elif direction.y() == 0: elif direction.y() == 0:
r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))
#r = 1.0/(abs(dt.m11()) + abs(dt.m21()))
else: else:
r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5
direction = direction * r directionr = direction * r
viewDir = Point(dt.map(direction) - dt.map(Point(0,0))) viewDir = Point(dt.map(directionr) - dt.map(Point(0,0)))
if viewDir.manhattanLength() == 0: if viewDir.manhattanLength() == 0:
return None, None ## pixel size cannot be represented on this scale return None, None ## pixel size cannot be represented on this scale
@ -182,7 +184,7 @@ class GraphicsItem(object):
normView = viewDir.norm() ## direction of one pixel orthogonal to line normView = viewDir.norm() ## direction of one pixel orthogonal to line
normOrtho = orthoDir.norm() normOrtho = orthoDir.norm()
except: except:
raise Exception("Invalid direction %s" %direction) raise Exception("Invalid direction %s" %directionr)
dti = fn.invertQTransform(dt) dti = fn.invertQTransform(dt)

View File

@ -0,0 +1,63 @@
from ..Qt import QtGui, QtCore
from ..Point import Point
class GraphicsWidgetAnchor:
"""
Class used to allow GraphicsWidgets to anchor to a specific position on their
parent.
"""
def __init__(self):
self.__parent = None
self.__parentAnchor = None
self.__itemAnchor = None
self.__offset = (0,0)
if hasattr(self, 'geometryChanged'):
self.geometryChanged.connect(self.__geometryChanged)
def anchor(self, itemPos, parentPos, offset=(0,0)):
"""
Anchors the item at its local itemPos to the item's parent at parentPos.
Both positions are expressed in values relative to the size of the item or parent;
a value of 0 indicates left or top edge, while 1 indicates right or bottom edge.
Optionally, offset may be specified to introduce an absolute offset.
Example: anchor a box such that its upper-right corner is fixed 10px left
and 10px down from its parent's upper-right corner::
box.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10))
"""
parent = self.parentItem()
if parent is None:
raise Exception("Cannot anchor; parent is not set.")
if self.__parent is not parent:
if self.__parent is not None:
self.__parent.geometryChanged.disconnect(self.__geometryChanged)
self.__parent = parent
parent.geometryChanged.connect(self.__geometryChanged)
self.__itemAnchor = itemPos
self.__parentAnchor = parentPos
self.__offset = offset
self.__geometryChanged()
def __geometryChanged(self):
if self.__parent is None:
return
if self.__itemAnchor is None:
return
o = self.mapToParent(Point(0,0))
a = self.boundingRect().bottomRight() * Point(self.__itemAnchor)
a = self.mapToParent(a)
p = self.__parent.boundingRect().bottomRight() * Point(self.__parentAnchor)
off = Point(self.__offset)
pos = p + (o-a) + off
self.setPos(pos)

View File

@ -2,12 +2,14 @@ from .GraphicsWidget import GraphicsWidget
from .LabelItem import LabelItem from .LabelItem import LabelItem
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from .. import functions as fn from .. import functions as fn
from ..Point import Point
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
__all__ = ['LegendItem'] __all__ = ['LegendItem']
class LegendItem(GraphicsWidget): class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
""" """
Displays a legend used for describing the contents of a plot. Displays a legend used for describing the contents of a plot.
LegendItems are most commonly created by calling PlotItem.addLegend().
Note that this item should not be added directly to a PlotItem. Instead, Note that this item should not be added directly to a PlotItem. Instead,
Make it a direct descendant of the PlotItem:: Make it a direct descendant of the PlotItem::
@ -15,17 +17,45 @@ class LegendItem(GraphicsWidget):
legend.setParentItem(plotItem) legend.setParentItem(plotItem)
""" """
def __init__(self, size, offset): def __init__(self, size=None, offset=None):
"""
========== ===============================================================
Arguments
size Specifies the fixed size (width, height) of the legend. If
this argument is omitted, the legend will autimatically resize
to fit its contents.
offset Specifies the offset position relative to the legend's parent.
Positive values offset from the left or top; negative values
offset from the right or bottom. If offset is None, the
legend must be anchored manually by calling anchor() or
positioned by calling setPos().
========== ===============================================================
"""
GraphicsWidget.__init__(self) GraphicsWidget.__init__(self)
GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemIgnoresTransformations) self.setFlag(self.ItemIgnoresTransformations)
self.layout = QtGui.QGraphicsGridLayout() self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout) self.setLayout(self.layout)
self.items = [] self.items = []
self.size = size self.size = size
self.offset = offset self.offset = offset
self.setGeometry(QtCore.QRectF(self.offset[0], self.offset[1], self.size[0], self.size[1])) if size is not None:
self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1]))
def addItem(self, item, title): def setParentItem(self, p):
ret = GraphicsWidget.setParentItem(self, p)
if self.offset is not None:
offset = Point(self.offset)
anchorx = 1 if offset[0] <= 0 else 0
anchory = 1 if offset[1] <= 0 else 0
anchor = (anchorx, anchory)
self.anchor(itemPos=anchor, parentPos=anchor, offset=offset)
return ret
def addItem(self, item, name):
""" """
Add a new entry to the legend. Add a new entry to the legend.
@ -36,15 +66,30 @@ class LegendItem(GraphicsWidget):
title The title to display for this item. Simple HTML allowed. title The title to display for this item. Simple HTML allowed.
=========== ======================================================== =========== ========================================================
""" """
label = LabelItem(title) label = LabelItem(name)
sample = ItemSample(item) sample = ItemSample(item)
row = len(self.items) row = len(self.items)
self.items.append((sample, label)) self.items.append((sample, label))
self.layout.addItem(sample, row, 0) self.layout.addItem(sample, row, 0)
self.layout.addItem(label, row, 1) self.layout.addItem(label, row, 1)
self.updateSize()
def updateSize(self):
if self.size is not None:
return
height = 0
width = 0
print "-------"
for sample, label in self.items:
height += max(sample.height(), label.height()) + 3
width = max(width, sample.width()+label.width())
print width, height
print width, height
self.setGeometry(0, 0, width+25, height)
def boundingRect(self): def boundingRect(self):
return QtCore.QRectF(0, 0, self.size[0], self.size[1]) return QtCore.QRectF(0, 0, self.width(), self.height())
def paint(self, p, *args): def paint(self, p, *args):
p.setPen(fn.mkPen(255,255,255,100)) p.setPen(fn.mkPen(255,255,255,100))
@ -61,8 +106,16 @@ class ItemSample(GraphicsWidget):
return QtCore.QRectF(0, 0, 20, 20) return QtCore.QRectF(0, 0, 20, 20)
def paint(self, p, *args): def paint(self, p, *args):
p.setPen(fn.mkPen(self.item.opts['pen'])) opts = self.item.opts
if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None:
p.setBrush(fn.mkBrush(opts['fillBrush']))
p.setPen(fn.mkPen(None))
p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)]))
p.setPen(fn.mkPen(opts['pen']))
p.drawLine(2, 18, 18, 2) p.drawLine(2, 18, 18, 2)

View File

@ -65,6 +65,7 @@ class PlotCurveItem(GraphicsObject):
'fillLevel': None, 'fillLevel': None,
'brush': None, 'brush': None,
'stepMode': False, 'stepMode': False,
'name': None
} }
self.setClickable(kargs.get('clickable', False)) self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -238,6 +239,9 @@ class PlotCurveItem(GraphicsObject):
self.fillPath = None self.fillPath = None
#self.xDisp = self.yDisp = None #self.xDisp = self.yDisp = None
if 'name' in kargs:
self.opts['name'] = kargs['name']
if 'pen' in kargs: if 'pen' in kargs:
self.setPen(kargs['pen']) self.setPen(kargs['pen'])
if 'shadowPen' in kargs: if 'shadowPen' in kargs:

View File

@ -317,6 +317,8 @@ class PlotDataItem(GraphicsObject):
## pull in all style arguments. ## pull in all style arguments.
## Use self.opts to fill in anything not present in kargs. ## Use self.opts to fill in anything not present in kargs.
if 'name' in kargs:
self.opts['name'] = kargs['name']
## if symbol pen/brush are given with no symbol, then assume symbol is 'o' ## if symbol pen/brush are given with no symbol, then assume symbol is 'o'

View File

@ -33,6 +33,7 @@ from .. PlotDataItem import PlotDataItem
from .. ViewBox import ViewBox from .. ViewBox import ViewBox
from .. AxisItem import AxisItem from .. AxisItem import AxisItem
from .. LabelItem import LabelItem from .. LabelItem import LabelItem
from .. LegendItem import LegendItem
from .. GraphicsWidget import GraphicsWidget from .. GraphicsWidget import GraphicsWidget
from .. ButtonItem import ButtonItem from .. ButtonItem import ButtonItem
from pyqtgraph.WidgetGroup import WidgetGroup from pyqtgraph.WidgetGroup import WidgetGroup
@ -528,6 +529,9 @@ class PlotItem(GraphicsWidget):
#c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged)
#self.plotChanged() #self.plotChanged()
name = kargs.get('name', getattr(item, 'opts', {}).get('name', None))
if name is not None and self.legend is not None:
self.legend.addItem(item, name=name)
def addDataItem(self, item, *args): def addDataItem(self, item, *args):
@ -596,6 +600,16 @@ class PlotItem(GraphicsWidget):
return item return item
def addLegend(self, size=None, offset=(30, 30)):
"""
Create a new LegendItem and anchor it over the internal ViewBox.
Plots will be automatically displayed in the legend if they
are created with the 'name' argument.
"""
self.legend = LegendItem(size, offset)
self.legend.setParentItem(self.vb)
return self.legend
def scatterPlot(self, *args, **kargs): def scatterPlot(self, *args, **kargs):
if 'pen' in kargs: if 'pen' in kargs:
kargs['symbolPen'] = kargs['pen'] kargs['symbolPen'] = kargs['pen']

View File

@ -166,7 +166,7 @@ class ViewBox(GraphicsWidget):
ViewBox.NamedViews[name] = self ViewBox.NamedViews[name] = self
ViewBox.updateAllViewLists() ViewBox.updateAllViewLists()
sid = id(self) sid = id(self)
self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if ViewBox is not None else None) self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None)
#self.destroyed.connect(self.unregister) #self.destroyed.connect(self.unregister)
def unregister(self): def unregister(self):
@ -1154,7 +1154,7 @@ class ViewBox(GraphicsWidget):
if any(changed): if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange']) self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self) self.sigTransformChanged.emit(self) ## segfaults here: 1
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
if self.border is not None: if self.border is not None:

View File

@ -1,5 +1,31 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import Transform3D from pyqtgraph import Transform3D
from OpenGL.GL import *
from OpenGL import GL
GLOptions = {
'opaque': {
GL_DEPTH_TEST: True,
GL_BLEND: False,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
},
'translucent': {
GL_DEPTH_TEST: True,
GL_BLEND: True,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA),
},
'additive': {
GL_DEPTH_TEST: False,
GL_BLEND: True,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE),
},
}
class GLGraphicsItem(QtCore.QObject): class GLGraphicsItem(QtCore.QObject):
def __init__(self, parentItem=None): def __init__(self, parentItem=None):
@ -11,6 +37,7 @@ class GLGraphicsItem(QtCore.QObject):
self.__visible = True self.__visible = True
self.setParentItem(parentItem) self.setParentItem(parentItem)
self.setDepthValue(0) self.setDepthValue(0)
self.__glOpts = {}
def setParentItem(self, item): def setParentItem(self, item):
if self.__parent is not None: if self.__parent is not None:
@ -23,7 +50,52 @@ class GLGraphicsItem(QtCore.QObject):
if self.view() is not None: if self.view() is not None:
self.view().removeItem(self) self.view().removeItem(self)
self.__parent.view().addItem(self) self.__parent.view().addItem(self)
def setGLOptions(self, opts):
"""
Set the OpenGL state options to use immediately before drawing this item.
(Note that subclasses must call setupGLState before painting for this to work)
The simplest way to invoke this method is to pass in the name of
a predefined set of options (see the GLOptions variable):
============= ======================================================
opaque Enables depth testing and disables blending
translucent Enables depth testing and blending
Elements must be drawn sorted back-to-front for
translucency to work correctly.
additive Disables depth testing, enables blending.
Colors are added together, so sorting is not required.
============= ======================================================
It is also possible to specify any arbitrary settings as a dictionary.
This may consist of {'functionName': (args...)} pairs where functionName must
be a callable attribute of OpenGL.GL, or {GL_STATE_VAR: bool} pairs
which will be interpreted as calls to glEnable or glDisable(GL_STATE_VAR).
For example::
{
GL_ALPHA_TEST: True,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA),
}
"""
if isinstance(opts, basestring):
opts = GLOptions[opts]
self.__glOpts = opts.copy()
def updateGLOptions(self, opts):
"""
Modify the OpenGL state options to use immediately before drawing this item.
*opts* must be a dictionary as specified by setGLOptions.
Values may also be None, in which case the key will be ignored.
"""
self.__glOpts.update(opts)
def parentItem(self): def parentItem(self):
return self.__parent return self.__parent
@ -135,13 +207,30 @@ class GLGraphicsItem(QtCore.QObject):
""" """
pass pass
def setupGLState(self):
"""
This method is responsible for preparing the GL state options needed to render
this item (blending, depth testing, etc). The method is called immediately before painting the item.
"""
for k,v in self.__glOpts.items():
if v is None:
continue
if isinstance(k, basestring):
func = getattr(GL, k)
func(*v)
else:
if v is True:
glEnable(k)
else:
glDisable(k)
def paint(self): def paint(self):
""" """
Called by the GLViewWidget to draw this item. Called by the GLViewWidget to draw this item.
It is the responsibility of the item to set up its own modelview matrix, It is the responsibility of the item to set up its own modelview matrix,
but the caller will take care of pushing/popping. but the caller will take care of pushing/popping.
""" """
pass self.setupGLState()
def update(self): def update(self):
v = self.view() v = self.view()

View File

@ -12,8 +12,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
- Export options - Export options
""" """
ShareWidget = None
def __init__(self, parent=None): def __init__(self, parent=None):
QtOpenGL.QGLWidget.__init__(self, parent) if GLViewWidget.ShareWidget is None:
## create a dummy widget to allow sharing objects (textures, shaders, etc) between views
GLViewWidget.ShareWidget = QtOpenGL.QGLWidget()
QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget)
self.setFocusPolicy(QtCore.Qt.ClickFocus) self.setFocusPolicy(QtCore.Qt.ClickFocus)
self.opts = { self.opts = {
@ -131,6 +139,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glMatrixMode(GL_MODELVIEW) glMatrixMode(GL_MODELVIEW)
glPopMatrix() glPopMatrix()
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
if distance is not None:
self.opts['distance'] = distance
if elevation is not None:
self.opts['elevation'] = elevation
if azimuth is not None:
self.opts['azimuth'] = azimuth
self.update()
def cameraPosition(self): def cameraPosition(self):
"""Return current position of camera based on center, dist, elevation, and azimuth""" """Return current position of camera based on center, dist, elevation, and azimuth"""

View File

@ -1,5 +1,6 @@
from pyqtgraph.Qt import QtGui from pyqtgraph.Qt import QtGui
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
import numpy as np
class MeshData(object): class MeshData(object):
""" """
@ -10,148 +11,400 @@ class MeshData(object):
- list of triangles - list of triangles
- colors per vertex, edge, or tri - colors per vertex, edge, or tri
- normals per vertex or tri - normals per vertex or tri
This class handles conversion between the standard [list of vertexes, list of faces]
format (suitable for use with glDrawElements) and 'indexed' [list of vertexes] format
(suitable for use with glDrawArrays). It will automatically compute face normal
vectors as well as averaged vertex normal vectors.
The class attempts to be as efficient as possible in caching conversion results and
avoiding unnecessary conversions.
""" """
def __init__(self): def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
self._vertexes = [] """
============= =====================================================
Arguments
vertexes (Nv, 3) array of vertex coordinates.
If faces is not specified, then this will instead be
interpreted as (Nf, 3, 3) array of coordinates.
faces (Nf, 3) array of indexes into the vertex array.
edges [not available yet]
vertexColors (Nv, 4) array of vertex colors.
If faces is not specified, then this will instead be
interpreted as (Nf, 3, 4) array of colors.
faceColors (Nf, 4) array of face colors.
============= =====================================================
All arguments are optional.
"""
self._vertexes = None # (Nv,3) array of vertex coordinates
self._vertexesIndexedByFaces = None # (Nf, 3, 3) array of vertex coordinates
self._vertexesIndexedByEdges = None # (Ne, 2, 3) array of vertex coordinates
## mappings between vertexes, faces, and edges
self._faces = None # Nx3 array of indexes into self._vertexes specifying three vertexes for each face
self._edges = None self._edges = None
self._faces = [] self._vertexFaces = None ## maps vertex ID to a list of face IDs (inverse mapping of _faces)
self._vertexFaces = None ## maps vertex ID to a list of face IDs self._vertexEdges = None ## maps vertex ID to a list of edge IDs (inverse mapping of _edges)
self._vertexNormals = None
self._faceNormals = None
self._vertexColors = None
self._edgeColors = None
self._faceColors = None
self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given
def setFaces(self, faces, vertexes=None): ## Per-vertex data
""" self._vertexNormals = None # (Nv, 3) array of normals, one per vertex
Set the faces in this data set. self._vertexNormalsIndexedByFaces = None # (Nf, 3, 3) array of normals
Data may be provided either as an Nx3x3 list of floats (9 float coordinate values per face):: self._vertexColors = None # (Nv, 3) array of colors
self._vertexColorsIndexedByFaces = None # (Nf, 3, 4) array of colors
self._vertexColorsIndexedByEdges = None # (Nf, 2, 4) array of colors
faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ] ## Per-face data
self._faceNormals = None # (Nf, 3) array of face normals
self._faceNormalsIndexedByFaces = None # (Nf, 3, 3) array of face normals
self._faceColors = None # (Nf, 4) array of face colors
self._faceColorsIndexedByFaces = None # (Nf, 3, 4) array of face colors
self._faceColorsIndexedByEdges = None # (Ne, 2, 4) array of face colors
## Per-edge data
self._edgeColors = None # (Ne, 4) array of edge colors
self._edgeColorsIndexedByEdges = None # (Ne, 2, 4) array of edge colors
#self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given
if vertexes is not None:
if faces is None:
self.setVertexes(vertexes, indexed='faces')
if vertexColors is not None:
self.setVertexColors(vertexColors, indexed='faces')
if faceColors is not None:
self.setFaceColors(faceColors, indexed='faces')
else:
self.setVertexes(vertexes)
self.setFaces(faces)
if vertexColors is not None:
self.setVertexColors(vertexColors)
if faceColors is not None:
self.setFaceColors(faceColors)
or as an Nx3 list of ints (vertex integers) AND an Mx3 list of floats (3 float coordinate values per vertex):: #self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors)
faces = [ (p1, p2, p3), ... ]
vertexes = [ (x, y, z), ... ]
"""
if vertexes is None: #def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None):
self._setUnindexedFaces(faces) #"""
#Set the faces in this data set.
#Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face)::
#faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ]
#or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex)::
#faces = [ (p1, p2, p3), ... ]
#vertexes = [ (x, y, z), ... ]
#"""
#if not isinstance(vertexes, np.ndarray):
#vertexes = np.array(vertexes)
#if vertexes.dtype != np.float:
#vertexes = vertexes.astype(float)
#if faces is None:
#self._setIndexedFaces(vertexes, vertexColors, faceColors)
#else:
#self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors)
##print self.vertexes().shape
##print self.faces().shape
#def setMeshColor(self, color):
#"""Set the color of the entire mesh. This removes any per-face or per-vertex colors."""
#color = fn.Color(color)
#self._meshColor = color.glColor()
#self._vertexColors = None
#self._faceColors = None
#def __iter__(self):
#"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
#vnorms = self.vertexNormals()
#vcolors = self.vertexColors()
#for i in range(self._faces.shape[0]):
#face = []
#for j in [0,1,2]:
#vind = self._faces[i,j]
#pos = self._vertexes[vind]
#norm = vnorms[vind]
#if vcolors is None:
#color = self._meshColor
#else:
#color = vcolors[vind]
#face.append((pos, norm, color))
#yield face
#def __len__(self):
#return len(self._faces)
def faces(self):
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh."""
return self._faces
def setFaces(self, faces):
"""Set the (Nf, 3) array of faces. Each rown in the array contains
three indexes into the vertex array, specifying the three corners
of a triangular face."""
self._faces = faces
self._vertexFaces = None
self._vertexesIndexedByFaces = None
self.resetNormals()
self._vertexColorsIndexedByFaces = None
self._faceColorsIndexedByFaces = None
def vertexes(self, indexed=None):
"""Return an array (N,3) of the positions of vertexes in the mesh.
By default, each unique vertex appears only once in the array.
If indexed is 'faces', then the array will instead contain three vertexes
per face in the mesh (and a single vertex may appear more than once in the array)."""
if indexed is None:
if self._vertexes is None and self._vertexesIndexedByFaces is not None:
self._computeUnindexedVertexes()
return self._vertexes
elif indexed == 'faces':
if self._vertexesIndexedByFaces is None and self._vertexes is not None:
self._vertexesIndexedByFaces = self._vertexes[self.faces()]
return self._vertexesIndexedByFaces
else: else:
self._setIndexedFaces(faces, vertexes) raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setVertexes(self, verts=None, indexed=None, resetNormals=True):
"""
Set the array (Nv, 3) of vertex coordinates.
If indexed=='faces', then the data must have shape (Nf, 3, 3) and is
assumed to be already indexed as a list of faces.
This will cause any pre-existing normal vectors to be cleared
unless resetNormals=False.
"""
if indexed is None:
if verts is not None:
self._vertexes = verts
self._vertexesIndexedByFaces = None
elif indexed=='faces':
self._vertexes = None
if verts is not None:
self._vertexesIndexedByFaces = verts
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
if resetNormals:
self.resetNormals()
def setMeshColor(self, color): def resetNormals(self):
"""Set the color of the entire mesh. This removes any per-face or per-vertex colors.""" self._vertexNormals = None
color = fn.Color(color) self._vertexNormalsIndexedByFaces = None
self._meshColor = color.glColor() self._faceNormals = None
self._vertexColors = None self._faceNormalsIndexedByFaces = None
self._faceColors = None
def hasFaceIndexedData(self):
"""Return True if this object already has vertex positions indexed by face"""
return self._vertexesIndexedByFaces is not None
def _setUnindexedFaces(self, faces): def hasEdgeIndexedData(self):
verts = {} return self._vertexesIndexedByEdges is not None
self._faces = []
def hasVertexColor(self):
"""Return True if this data set has vertex color information"""
for v in (self._vertexColors, self._vertexColorsIndexedByFaces, self._vertexColorsIndexedByEdges):
if v is not None:
return True
return False
def hasFaceColor(self):
"""Return True if this data set has face color information"""
for v in (self._faceColors, self._faceColorsIndexedByFaces, self._faceColorsIndexedByEdges):
if v is not None:
return True
return False
def faceNormals(self, indexed=None):
"""
Return an array (Nf, 3) of normal vectors for each face.
If indexed='faces', then instead return an indexed array
(Nf, 3, 3) (this is just the same array with each vector
copied three times).
"""
if self._faceNormals is None:
v = self.vertexes(indexed='faces')
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
if indexed is None:
return self._faceNormals
elif indexed == 'faces':
if self._faceNormalsIndexedByFaces is None:
norms = np.empty((self._faceNormals.shape[0], 3, 3))
norms[:] = self._faceNormals[:,np.newaxis,:]
self._faceNormalsIndexedByFaces = norms
return self._faceNormalsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def vertexNormals(self, indexed=None):
"""
Return an array of normal vectors.
By default, the array will be (N, 3) with one entry per unique vertex in the mesh.
If indexed is 'faces', then the array will contain three normal vectors per face
(and some vertexes may be repeated).
"""
if self._vertexNormals is None:
faceNorms = self.faceNormals()
vertFaces = self.vertexFaces()
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
for vindex in xrange(self._vertexes.shape[0]):
norms = faceNorms[vertFaces[vindex]] ## get all face normals
norm = norms.sum(axis=0) ## sum normals
norm /= (norm**2).sum()**0.5 ## and re-normalize
self._vertexNormals[vindex] = norm
if indexed is None:
return self._vertexNormals
elif indexed == 'faces':
return self._vertexNormals[self.faces()]
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def vertexColors(self, indexed=None):
"""
Return an array (Nv, 4) of vertex colors.
If indexed=='faces', then instead return an indexed array
(Nf, 3, 4).
"""
if indexed is None:
return self._vertexColors
elif indexed == 'faces':
if self._vertexColorsIndexedByFaces is None:
self._vertexColorsIndexedByFaces = self._vertexColors[self.faces()]
return self._vertexColorsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setVertexColors(self, colors, indexed=None):
"""
Set the vertex color array (Nv, 4).
If indexed=='faces', then the array will be interpreted
as indexed and should have shape (Nf, 3, 4)
"""
if indexed is None:
self._vertexColors = colors
self._vertexColorsIndexedByFaces = None
elif indexed == 'faces':
self._vertexColors = None
self._vertexColorsIndexedByFaces = colors
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def faceColors(self, indexed=None):
"""
Return an array (Nf, 4) of face colors.
If indexed=='faces', then instead return an indexed array
(Nf, 3, 4) (note this is just the same array with each color
repeated three times).
"""
if indexed is None:
return self._faceColors
elif indexed == 'faces':
if self._faceColorsIndexedByFaces is None and self._faceColors is not None:
Nf = self._faceColors.shape[0]
self._faceColorsIndexedByFaces = np.empty((Nf, 3, 4), dtype=self._faceColors.dtype)
self._faceColorsIndexedByFaces[:] = self._faceColors.reshape(Nf, 1, 4)
return self._faceColorsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setFaceColors(self, colors, indexed=None):
"""
Set the face color array (Nf, 4).
If indexed=='faces', then the array will be interpreted
as indexed and should have shape (Nf, 3, 4)
"""
if indexed is None:
self._faceColors = colors
self._faceColorsIndexedByFaces = None
elif indexed == 'faces':
self._faceColors = None
self._faceColorsIndexedByFaces = colors
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def faceCount(self):
"""
Return the number of faces in the mesh.
"""
if self._faces is not None:
return self._faces.shape[0]
elif self._vertexesIndexedByFaces is not None:
return self._vertexesIndexedByFaces.shape[0]
def edgeColors(self):
return self._edgeColors
#def _setIndexedFaces(self, faces, vertexColors=None, faceColors=None):
#self._vertexesIndexedByFaces = faces
#self._vertexColorsIndexedByFaces = vertexColors
#self._faceColorsIndexedByFaces = faceColors
def _computeUnindexedVertexes(self):
## Given (Nv, 3, 3) array of vertexes-indexed-by-face, convert backward to unindexed vertexes
## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14)
## I think generally this should be discouraged..
faces = self._vertexesIndexedByFaces
verts = {} ## used to remember the index of each vertex position
self._faces = np.empty(faces.shape[:2], dtype=np.uint)
self._vertexes = [] self._vertexes = []
self._vertexFaces = [] self._vertexFaces = []
self._faceNormals = None self._faceNormals = None
self._vertexNormals = None self._vertexNormals = None
for face in faces: for i in xrange(faces.shape[0]):
face = faces[i]
inds = [] inds = []
for pt in face: for j in range(face.shape[0]):
pt = face[j]
pt2 = tuple([round(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged pt2 = tuple([round(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged
index = verts.get(pt2, None) index = verts.get(pt2, None)
if index is None: if index is None:
self._vertexes.append(QtGui.QVector3D(*pt)) #self._vertexes.append(QtGui.QVector3D(*pt))
self._vertexes.append(pt)
self._vertexFaces.append([]) self._vertexFaces.append([])
index = len(self._vertexes)-1 index = len(self._vertexes)-1
verts[pt2] = index verts[pt2] = index
self._vertexFaces[index].append(len(self._faces)) self._vertexFaces[index].append(i) # keep track of which vertexes belong to which faces
inds.append(index) self._faces[i,j] = index
self._faces.append(tuple(inds)) self._vertexes = np.array(self._vertexes, dtype=float)
def _setIndexedFaces(self, faces, vertexes): #def _setUnindexedFaces(self, faces, vertexes, vertexColors=None, faceColors=None):
self._vertexes = [QtGui.QVector3D(*v) for v in vertexes] #self._vertexes = vertexes #[QtGui.QVector3D(*v) for v in vertexes]
self._faces = faces #self._faces = faces.astype(np.uint)
self._edges = None #self._edges = None
self._vertexFaces = None #self._vertexFaces = None
self._faceNormals = None #self._faceNormals = None
self._vertexNormals = None #self._vertexNormals = None
#self._vertexColors = vertexColors
#self._faceColors = faceColors
def vertexFaces(self): def vertexFaces(self):
""" """
Return list mapping each vertex index to a list of face indexes that use the vertex. Return list mapping each vertex index to a list of face indexes that use the vertex.
""" """
if self._vertexFaces is None: if self._vertexFaces is None:
self._vertexFaces = [[]] * len(self._vertexes) self._vertexFaces = [None] * len(self.vertexes())
for i, face in enumerate(self._faces): for i in xrange(self._faces.shape[0]):
face = self._faces[i]
for ind in face: for ind in face:
if len(self._vertexFaces[ind]) == 0: if self._vertexFaces[ind] is None:
self._vertexFaces[ind] = [] ## need a unique/empty list to fill self._vertexFaces[ind] = [] ## need a unique/empty list to fill
self._vertexFaces[ind].append(i) self._vertexFaces[ind].append(i)
return self._vertexFaces return self._vertexFaces
def __iter__(self):
"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
vnorms = self.vertexNormals()
vcolors = self.vertexColors()
for i in range(len(self._faces)):
face = []
for j in [0,1,2]:
vind = self._faces[i][j]
pos = self._vertexes[vind]
norm = vnorms[vind]
if vcolors is None:
color = self._meshColor
else:
color = vcolors[vind]
face.append((pos, norm, color))
yield face
def faceNormals(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 = [self._vertexes[vind] for vind in face]
norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0])
norm = norm / norm.length() ## don't use .normalized(); doesn't work for small values.
self._faceNormals.append(norm)
return self._faceNormals
def vertexNormals(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.faceNormals()
vertFaces = self.vertexFaces()
self._vertexNormals = []
for vindex in range(len(self._vertexes)):
#print vertFaces[vindex]
norms = [faceNorms[findex] for findex in vertFaces[vindex]]
norm = QtGui.QVector3D()
for fn in norms:
norm += fn
norm = norm / norm.length() ## don't use .normalize(); doesn't work for small values.
self._vertexNormals.append(norm)
return self._vertexNormals
def vertexColors(self):
return self._vertexColors
def faceColors(self):
return self._faceColors
def edgeColors(self):
return self._edgeColors
#def reverseNormals(self): #def reverseNormals(self):
#""" #"""
#Reverses the direction of all normal vectors. #Reverses the direction of all normal vectors.
@ -168,7 +421,21 @@ class MeshData(object):
def save(self): def save(self):
"""Serialize this mesh to a string appropriate for disk storage""" """Serialize this mesh to a string appropriate for disk storage"""
import pickle import pickle
names = ['_vertexes', '_edges', '_faces', '_vertexFaces', '_vertexNormals', '_faceNormals', '_vertexColors', '_edgeColors', '_faceColors', '_meshColor'] if self._faces is not None:
names = ['_vertexes', '_faces']
else:
names = ['_vertexesIndexedByFaces']
if self._vertexColors is not None:
names.append('_vertexColors')
elif self._vertexColorsIndexedByFaces is not None:
names.append('_vertexColorsIndexedByFaces')
if self._faceColors is not None:
names.append('_faceColors')
elif self._faceColorsIndexedByFaces is not None:
names.append('_faceColorsIndexedByFaces')
state = {n:getattr(self, n) for n in names} state = {n:getattr(self, n) for n in names}
return pickle.dumps(state) return pickle.dumps(state)
@ -178,6 +445,45 @@ class MeshData(object):
state = pickle.loads(state) state = pickle.loads(state)
for k in state: for k in state:
setattr(self, k, state[k]) setattr(self, k, state[k])
def sphere(rows, cols, radius=1.0, offset=True):
"""
Return a MeshData instance with vertexes and faces computed
for a spherical surface.
"""
verts = np.empty((rows+1, cols, 3), dtype=float)
## compute vertexes
phi = (np.arange(rows+1) * np.pi / rows).reshape(rows+1, 1)
s = radius * np.sin(phi)
verts[...,2] = radius * np.cos(phi)
th = ((np.arange(cols) * 2 * np.pi / cols).reshape(1, cols))
if offset:
th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column
verts[...,0] = s * np.cos(th)
verts[...,1] = s * np.sin(th)
verts = verts.reshape((rows+1)*cols, 3)[cols-1:-(cols-1)] ## remove redundant vertexes from top and bottom
## compute faces
faces = np.empty((rows*cols*2, 3), dtype=np.uint)
rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]])
rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]])
for row in range(rows):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * cols
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
faces = faces[cols:-cols] ## cut off zero-area triangles at top and bottom
## adjust for redundant vertexes that were removed from top and bottom
vmin = cols-1
faces[faces<vmin] = vmin
faces -= vmin
vmax = verts.shape[0]-1
faces[faces>vmax] = vmax
return MeshData(vertexes=verts, faces=faces)

View File

@ -11,8 +11,9 @@ class GLGridItem(GLGraphicsItem):
Displays a wire-grame grid. Displays a wire-grame grid.
""" """
def __init__(self, size=None, color=None): def __init__(self, size=None, color=None, glOptions='translucent'):
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
if size is None: if size is None:
size = QtGui.QVector3D(1,1,1) size = QtGui.QVector3D(1,1,1)
self.setSize(size=size) self.setSize(size=size)
@ -34,10 +35,10 @@ class GLGridItem(GLGraphicsItem):
def paint(self): def paint(self):
self.setupGLState()
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND ) #glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST ) #glEnable( GL_ALPHA_TEST )
glEnable( GL_POINT_SMOOTH ) glEnable( GL_POINT_SMOOTH )
#glDisable( GL_DEPTH_TEST ) #glDisable( GL_DEPTH_TEST )
glBegin( GL_LINES ) glBegin( GL_LINES )

View File

@ -16,65 +16,161 @@ class GLMeshItem(GLGraphicsItem):
Displays a 3D triangle mesh. Displays a 3D triangle mesh.
""" """
def __init__(self, faces, vertexes=None): def __init__(self, **kwds):
""" """
See :class:`MeshData <pyqtgraph.opengl.MeshData>` for initialization arguments. ============== =====================================================
Arguments
meshdata MeshData object from which to determine geometry for
this item.
color Default color used if no vertex or face colors are
specified.
shader Name of shader program to use (None for no shader)
smooth If True, normal vectors are computed for each vertex
and interpolated within each face.
computeNormals If False, then computation of normal vectors is
disabled. This can provide a performance boost for
meshes that do not make use of normals.
============== =====================================================
""" """
if isinstance(faces, MeshData): self.opts = {
self.data = faces 'meshdata': None,
else: 'color': (1., 1., 1., 1.),
self.data = MeshData() 'shader': None,
self.data.setFaces(faces, vertexes) 'smooth': True,
'computeNormals': True,
}
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
glopts = kwds.pop('glOptions', 'opaque')
self.setGLOptions(glopts)
shader = kwds.pop('shader', None)
self.setShader(shader)
def initializeGL(self): self.setMeshData(**kwds)
self.shader = shaders.getShaderProgram('balloon')
l = glGenLists(1) ## storage for data compiled from MeshData object
self.triList = l self.vertexes = None
glNewList(l, GL_COMPILE) self.normals = None
self.colors = None
self.faces = None
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) def setShader(self, shader):
glEnable( GL_BLEND ) self.opts['shader'] = shader
glEnable( GL_ALPHA_TEST ) self.update()
#glAlphaFunc( GL_ALWAYS,0.5 )
glEnable( GL_POINT_SMOOTH )
glDisable( GL_DEPTH_TEST )
glColor4f(1, 1, 1, .1)
glBegin( GL_TRIANGLES )
for face in self.data:
for (pos, norm, color) in face:
glColor4f(*color)
glNormal3f(norm.x(), norm.y(), norm.z())
glVertex3f(pos.x(), pos.y(), pos.z())
glEnd()
glEndList()
def shader(self):
return shaders.getShaderProgram(self.opts['shader'])
#l = glGenLists(1) def setMeshData(self, **kwds):
#self.meshList = l """
#glNewList(l, GL_COMPILE) Set mesh data for this item. This can be invoked two ways:
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND ) 1. Specify *meshdata* argument with a new MeshData object
#glEnable( GL_ALPHA_TEST ) 2. Specify keyword arguments to be passed to MeshData(..) to create a new instance.
##glAlphaFunc( GL_ALWAYS,0.5 ) """
#glEnable( GL_POINT_SMOOTH ) md = kwds.get('meshdata', None)
#glEnable( GL_DEPTH_TEST ) if md is None:
#glColor4f(1, 1, 1, .3) opts = {}
#glBegin( GL_LINES ) for k in ['vertexes', 'faces', 'edges', 'vertexColors', 'faceColors']:
#for f in self.faces: try:
#for i in [0,1,2]: opts[k] = kwds.pop(k)
#j = (i+1) % 3 except KeyError:
#glVertex3f(*f[i]) pass
#glVertex3f(*f[j]) md = MeshData(**opts)
#glEnd()
#glEndList() self.opts['meshdata'] = md
self.opts.update(kwds)
self.meshDataChanged()
self.update()
def meshDataChanged(self):
"""
This method must be called to inform the item that the MeshData object
has been altered.
"""
self.vertexes = None
self.faces = None
self.normals = None
self.colors = None
self.update()
def parseMeshData(self):
## interpret vertex / normal data before drawing
## This can:
## - automatically generate normals if they were not specified
## - pull vertexes/noormals/faces from MeshData if that was specified
if self.vertexes is not None and self.normals is not None:
return
#if self.opts['normals'] is None:
#if self.opts['meshdata'] is None:
#self.opts['meshdata'] = MeshData(vertexes=self.opts['vertexes'], faces=self.opts['faces'])
if self.opts['meshdata'] is not None:
md = self.opts['meshdata']
if self.opts['smooth'] and not md.hasFaceIndexedData():
self.vertexes = md.vertexes()
if self.opts['computeNormals']:
self.normals = md.vertexNormals()
self.faces = md.faces()
if md.hasVertexColor():
self.colors = md.vertexColors()
if md.hasFaceColor():
self.colors = md.faceColors()
else:
self.vertexes = md.vertexes(indexed='faces')
if self.opts['computeNormals']:
if self.opts['smooth']:
self.normals = md.vertexNormals(indexed='faces')
else:
self.normals = md.faceNormals(indexed='faces')
self.faces = None
if md.hasVertexColor():
self.colors = md.vertexColors(indexed='faces')
elif md.hasFaceColor():
self.colors = md.faceColors(indexed='faces')
return
def paint(self): def paint(self):
with self.shader: self.setupGLState()
glCallList(self.triList)
#shaders.glUseProgram(self.shader) self.parseMeshData()
#glCallList(self.triList)
#shaders.glUseProgram(0) with self.shader():
#glCallList(self.meshList) verts = self.vertexes
norms = self.normals
color = self.colors
faces = self.faces
if verts is None:
return
glEnableClientState(GL_VERTEX_ARRAY)
try:
glVertexPointerf(verts)
if self.colors is None:
color = self.opts['color']
if isinstance(color, QtGui.QColor):
glColor4f(*fn.glColor(color))
else:
glColor4f(*color)
else:
glEnableClientState(GL_COLOR_ARRAY)
glColorPointerf(color)
if norms is not None:
glEnableClientState(GL_NORMAL_ARRAY)
glNormalPointerf(norms)
if faces is None:
glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1]))
else:
faces = faces.astype(np.uint).flatten()
glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces)
finally:
glDisableClientState(GL_NORMAL_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)

View File

@ -12,6 +12,8 @@ class GLScatterPlotItem(GLGraphicsItem):
def __init__(self, **kwds): def __init__(self, **kwds):
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
glopts = kwds.pop('glOptions', 'additive')
self.setGLOptions(glopts)
self.pos = [] self.pos = []
self.size = 10 self.size = 10
self.color = [1.0,1.0,1.0,0.5] self.color = [1.0,1.0,1.0,0.5]
@ -71,27 +73,27 @@ class GLScatterPlotItem(GLGraphicsItem):
glBindTexture(GL_TEXTURE_2D, self.pointTexture) glBindTexture(GL_TEXTURE_2D, self.pointTexture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pData.shape[0], pData.shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, pData) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pData.shape[0], pData.shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, pData)
self.shader = shaders.getShaderProgram('point_sprite') self.shader = shaders.getShaderProgram('pointSprite')
#def getVBO(self, name): #def getVBO(self, name):
#if name not in self.vbo: #if name not in self.vbo:
#self.vbo[name] = vbo.VBO(getattr(self, name).astype('f')) #self.vbo[name] = vbo.VBO(getattr(self, name).astype('f'))
#return self.vbo[name] #return self.vbo[name]
def setupGLState(self): #def setupGLState(self):
"""Prepare OpenGL state for drawing. This function is called immediately before painting.""" #"""Prepare OpenGL state for drawing. This function is called immediately before painting."""
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly. ##glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly.
glBlendFunc(GL_SRC_ALPHA, GL_ONE) #glBlendFunc(GL_SRC_ALPHA, GL_ONE)
glEnable( GL_BLEND ) #glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST ) #glEnable( GL_ALPHA_TEST )
glDisable( GL_DEPTH_TEST ) #glDisable( GL_DEPTH_TEST )
#glEnable( GL_POINT_SMOOTH ) ##glEnable( GL_POINT_SMOOTH )
#glHint(GL_POINT_SMOOTH_HINT, GL_NICEST) ##glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
#glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3)) ##glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3))
#glPointParameterfv(GL_POINT_SIZE_MAX, (65500,)) ##glPointParameterfv(GL_POINT_SIZE_MAX, (65500,))
#glPointParameterfv(GL_POINT_SIZE_MIN, (0,)) ##glPointParameterfv(GL_POINT_SIZE_MIN, (0,))
def paint(self): def paint(self):
self.setupGLState() self.setupGLState()
@ -139,7 +141,7 @@ class GLScatterPlotItem(GLGraphicsItem):
glNormalPointerf(norm) glNormalPointerf(norm)
else: else:
glNormal3f(self.size,0,0) glNormal3f(self.size, 0, 0) ## vertex shader uses norm.x to determine point size
#glPointSize(self.size) #glPointSize(self.size)
glDrawArrays(GL_POINTS, 0, len(self.pos)) glDrawArrays(GL_POINTS, 0, len(self.pos))
finally: finally:

View File

@ -0,0 +1,139 @@
from OpenGL.GL import *
from GLMeshItem import GLMeshItem
from .. MeshData import MeshData
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
import numpy as np
__all__ = ['GLSurfacePlotItem']
class GLSurfacePlotItem(GLMeshItem):
"""
**Bases:** :class:`GLMeshItem <pyqtgraph.opengl.GLMeshItem>`
Displays a surface plot on a regular x,y grid
"""
def __init__(self, x=None, y=None, z=None, colors=None, **kwds):
"""
The x, y, z, and colors arguments are passed to setData().
All other keyword arguments are passed to GLMeshItem.__init__().
"""
self._x = None
self._y = None
self._z = None
self._color = None
self._vertexes = None
self._meshdata = MeshData()
GLMeshItem.__init__(self, meshdata=self._meshdata, **kwds)
self.setData(x, y, z, colors)
def setData(self, x=None, y=None, z=None, colors=None):
"""
Update the data in this surface plot.
========== =====================================================================
Arguments
x,y 1D arrays of values specifying the x,y positions of vertexes in the
grid. If these are omitted, then the values will be assumed to be
integers.
z 2D array of height values for each grid vertex.
colors (width, height, 4) array of vertex colors.
========== =====================================================================
All arguments are optional.
Note that if vertex positions are updated, the normal vectors for each triangle must
be recomputed. This is somewhat expensive if the surface was initialized with smooth=False
and very expensive if smooth=True. For faster performance, initialize with
computeNormals=False and use per-vertex colors or a normal-independent shader program.
"""
if x is not None:
if self._x is None or len(x) != len(self._x):
self._vertexes = None
self._x = x
if y is not None:
if self._y is None or len(y) != len(self._y):
self._vertexes = None
self._y = y
if z is not None:
#if self._x is None:
#self._x = np.arange(z.shape[0])
#self._vertexes = None
#if self._y is None:
#self._y = np.arange(z.shape[1])
#self._vertexes = None
if self._x is not None and z.shape[0] != len(self._x):
raise Exception('Z values must have shape (len(x), len(y))')
if self._y is not None and z.shape[1] != len(self._y):
raise Exception('Z values must have shape (len(x), len(y))')
self._z = z
if self._vertexes is not None and self._z.shape != self._vertexes.shape[:2]:
self._vertexes = None
if colors is not None:
self._colors = colors
self._meshdata.setVertexColors(colors)
if self._z is None:
return
updateMesh = False
newVertexes = False
## Generate vertex and face array
if self._vertexes is None:
newVertexes = True
self._vertexes = np.empty((self._z.shape[0], self._z.shape[1], 3), dtype=float)
self.generateFaces()
self._meshdata.setFaces(self._faces)
updateMesh = True
## Copy x, y, z data into vertex array
if newVertexes or x is not None:
if x is None:
if self._x is None:
x = np.arange(self._z.shape[0])
else:
x = self._x
self._vertexes[:, :, 0] = x.reshape(len(x), 1)
updateMesh = True
if newVertexes or y is not None:
if y is None:
if self._y is None:
y = np.arange(self._z.shape[1])
else:
y = self._y
self._vertexes[:, :, 1] = y.reshape(1, len(y))
updateMesh = True
if newVertexes or z is not None:
self._vertexes[...,2] = self._z
updateMesh = True
## Update MeshData
if updateMesh:
self._meshdata.setVertexes(self._vertexes.reshape(self._vertexes.shape[0]*self._vertexes.shape[1], 3))
self.meshDataChanged()
def generateFaces(self):
cols = self._z.shape[0]-1
rows = self._z.shape[1]-1
faces = np.empty((cols*rows*2, 3), dtype=np.uint)
rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]])
rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]])
for row in range(rows):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * (cols+1)
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * (cols+1)
self._faces = faces

View File

@ -13,7 +13,7 @@ class GLVolumeItem(GLGraphicsItem):
""" """
def __init__(self, data, sliceDensity=1, smooth=True): def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent'):
""" """
============== ======================================================================================= ============== =======================================================================================
**Arguments:** **Arguments:**
@ -27,6 +27,7 @@ class GLVolumeItem(GLGraphicsItem):
self.smooth = smooth self.smooth = smooth
self.data = data self.data = data
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
def initializeGL(self): def initializeGL(self):
glEnable(GL_TEXTURE_3D) glEnable(GL_TEXTURE_3D)
@ -62,15 +63,16 @@ class GLVolumeItem(GLGraphicsItem):
def paint(self): def paint(self):
self.setupGLState()
glEnable(GL_TEXTURE_3D) glEnable(GL_TEXTURE_3D)
glBindTexture(GL_TEXTURE_3D, self.texture) glBindTexture(GL_TEXTURE_3D, self.texture)
glEnable(GL_DEPTH_TEST) #glEnable(GL_DEPTH_TEST)
#glDisable(GL_CULL_FACE) #glDisable(GL_CULL_FACE)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND ) #glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST ) #glEnable( GL_ALPHA_TEST )
glColor4f(1,1,1,1) glColor4f(1,1,1,1)
view = self.view() view = self.view()

View File

@ -1,18 +1,22 @@
from OpenGL.GL import * from OpenGL.GL import *
from OpenGL.GL import shaders from OpenGL.GL import shaders
import re
## For centralizing and managing vertex/fragment shader programs. ## For centralizing and managing vertex/fragment shader programs.
def initShaders(): def initShaders():
global Shaders global Shaders
Shaders = [ Shaders = [
ShaderProgram('balloon', [ ## increases fragment alpha as the normal turns orthogonal to the view ShaderProgram(None, []),
## increases fragment alpha as the normal turns orthogonal to the view
## this is useful for viewing shells that enclose a volume (such as isosurfaces)
ShaderProgram('balloon', [
VertexShader(""" VertexShader("""
varying vec3 normal; varying vec3 normal;
void main() { void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal); normal = normalize(gl_NormalMatrix * gl_Normal);
//vec4 color = normal;
//normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0);
gl_FrontColor = gl_Color; gl_FrontColor = gl_Color;
gl_BackColor = gl_Color; gl_BackColor = gl_Color;
gl_Position = ftransform(); gl_Position = ftransform();
@ -27,7 +31,154 @@ def initShaders():
} }
""") """)
]), ]),
ShaderProgram('point_sprite', [ ## allows specifying point size using normal.x
## colors fragments based on face normals relative to view
## This means that the colors will change depending on how the view is rotated
ShaderProgram('viewNormalColor', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
color.x = (normal.x + 1) * 0.5;
color.y = (normal.y + 1) * 0.5;
color.z = (normal.z + 1) * 0.5;
gl_FragColor = color;
}
""")
]),
## colors fragments based on absolute face normals.
ShaderProgram('normalColor', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
color.x = (normal.x + 1) * 0.5;
color.y = (normal.y + 1) * 0.5;
color.z = (normal.z + 1) * 0.5;
gl_FragColor = color;
}
""")
]),
## very simple simulation of lighting.
## The light source position is always relative to the camera.
ShaderProgram('shaded', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
float p = dot(normal, normalize(vec3(1, -1, -1)));
p = p < 0. ? 0. : p * 0.8;
vec4 color = gl_Color;
color.x = color.x * (0.2 + p);
color.y = color.y * (0.2 + p);
color.z = color.z * (0.2 + p);
gl_FragColor = color;
}
""")
]),
## colors get brighter near edges of object
ShaderProgram('edgeHilight', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
float s = pow(normal.x*normal.x + normal.y*normal.y, 2.0);
color.x = color.x + s * (1.0-color.x);
color.y = color.y + s * (1.0-color.y);
color.z = color.z + s * (1.0-color.z);
gl_FragColor = color;
}
""")
]),
## colors fragments by z-value.
## This is useful for coloring surface plots by height.
## This shader uses a uniform called "colorMap" to determine how to map the colors:
## red = pow(z * colorMap[0] + colorMap[1], colorMap[2])
## green = pow(z * colorMap[3] + colorMap[4], colorMap[5])
## blue = pow(z * colorMap[6] + colorMap[7], colorMap[8])
## (set the values like this: shader['uniformMap'] = array([...])
ShaderProgram('heightColor', [
VertexShader("""
varying vec4 pos;
void main() {
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
pos = gl_Vertex;
gl_Position = ftransform();
}
"""),
FragmentShader("""
#version 140 // required for uniform blocks
uniform float colorMap[9];
varying vec4 pos;
out vec4 gl_FragColor;
in vec4 gl_Color;
void main() {
vec4 color = gl_Color;
color.x = colorMap[0] * (pos.z + colorMap[1]);
if (colorMap[2] != 1.0)
color.x = pow(color.x, colorMap[2]);
color.x = color.x < 0 ? 0 : (color.x > 1 ? 1 : color.x);
color.y = colorMap[3] * (pos.z + colorMap[4]);
if (colorMap[5] != 1.0)
color.y = pow(color.y, colorMap[5]);
color.y = color.y < 0 ? 0 : (color.y > 1 ? 1 : color.y);
color.z = colorMap[6] * (pos.z + colorMap[7]);
if (colorMap[8] != 1.0)
color.z = pow(color.z, colorMap[8]);
color.z = color.z < 0 ? 0 : (color.z > 1 ? 1 : color.z);
color.w = 1.0;
gl_FragColor = color;
}
"""),
], uniforms={'colorMap': [1, 1, 1, 1, 0.5, 1, 1, 0, 1]}),
ShaderProgram('pointSprite', [ ## allows specifying point size using normal.x
## See: ## See:
## ##
## http://stackoverflow.com/questions/9609423/applying-part-of-a-texture-sprite-sheet-texture-map-to-a-point-sprite-in-ios ## http://stackoverflow.com/questions/9609423/applying-part-of-a-texture-sprite-sheet-texture-map-to-a-point-sprite-in-ios
@ -58,52 +209,186 @@ CompiledShaderPrograms = {}
def getShaderProgram(name): def getShaderProgram(name):
return ShaderProgram.names[name] return ShaderProgram.names[name]
class VertexShader: class Shader:
def __init__(self, code): def __init__(self, shaderType, code):
self.shaderType = shaderType
self.code = code self.code = code
self.compiled = None self.compiled = None
def shader(self): def shader(self):
if self.compiled is None: if self.compiled is None:
self.compiled = shaders.compileShader(self.code, GL_VERTEX_SHADER) try:
self.compiled = shaders.compileShader(self.code, self.shaderType)
except RuntimeError as exc:
## Format compile errors a bit more nicely
if len(exc.args) == 3:
err, code, typ = exc.args
if not err.startswith('Shader compile failure'):
raise
code = code[0].split('\n')
err, c, msgs = err.partition(':')
err = err + '\n'
msgs = msgs.split('\n')
errNums = [()] * len(code)
for i, msg in enumerate(msgs):
msg = msg.strip()
if msg == '':
continue
m = re.match(r'\d+\((\d+)\)', msg)
if m is not None:
line = int(m.groups()[0])
errNums[line-1] = errNums[line-1] + (str(i+1),)
#code[line-1] = '%d\t%s' % (i+1, code[line-1])
err = err + "%d %s\n" % (i+1, msg)
errNums = [','.join(n) for n in errNums]
maxlen = max(map(len, errNums))
code = [errNums[i] + " "*(maxlen-len(errNums[i])) + line for i, line in enumerate(code)]
err = err + '\n'.join(code)
raise Exception(err)
else:
raise
return self.compiled return self.compiled
class FragmentShader: class VertexShader(Shader):
def __init__(self, code): def __init__(self, code):
self.code = code Shader.__init__(self, GL_VERTEX_SHADER, code)
self.compiled = None
class FragmentShader(Shader):
def __init__(self, code):
Shader.__init__(self, GL_FRAGMENT_SHADER, code)
def shader(self):
if self.compiled is None:
self.compiled = shaders.compileShader(self.code, GL_FRAGMENT_SHADER)
return self.compiled
class ShaderProgram: class ShaderProgram:
names = {} names = {}
def __init__(self, name, shaders): def __init__(self, name, shaders, uniforms=None):
self.name = name self.name = name
ShaderProgram.names[name] = self ShaderProgram.names[name] = self
self.shaders = shaders self.shaders = shaders
self.prog = None self.prog = None
self.blockData = {}
self.uniformData = {}
## parse extra options from the shader definition
if uniforms is not None:
for k,v in uniforms.items():
self[k] = v
def setBlockData(self, blockName, data):
if data is None:
del self.blockData[blockName]
else:
self.blockData[blockName] = data
def setUniformData(self, uniformName, data):
if data is None:
del self.uniformData[uniformName]
else:
self.uniformData[uniformName] = data
def __setitem__(self, item, val):
self.setUniformData(item, val)
def __delitem__(self, item):
self.setUniformData(item, None)
def program(self): def program(self):
if self.prog is None: if self.prog is None:
compiled = [s.shader() for s in self.shaders] ## compile all shaders try:
self.prog = shaders.compileProgram(*compiled) ## compile program compiled = [s.shader() for s in self.shaders] ## compile all shaders
self.prog = shaders.compileProgram(*compiled) ## compile program
except:
self.prog = -1
raise
return self.prog return self.prog
def __enter__(self): def __enter__(self):
glUseProgram(self.program()) if len(self.shaders) > 0 and self.program() != -1:
glUseProgram(self.program())
try:
## load uniform values into program
for uniformName, data in self.uniformData.items():
loc = self.uniform(uniformName)
if loc == -1:
raise Exception('Could not find uniform variable "%s"' % uniformName)
glUniform1fv(loc, len(data), data)
### bind buffer data to program blocks
#if len(self.blockData) > 0:
#bindPoint = 1
#for blockName, data in self.blockData.items():
### Program should have a uniform block declared:
###
### layout (std140) uniform blockName {
### vec4 diffuse;
### };
### pick any-old binding point. (there are a limited number of these per-program
#bindPoint = 1
### get the block index for a uniform variable in the shader
#blockIndex = glGetUniformBlockIndex(self.program(), blockName)
### give the shader block a binding point
#glUniformBlockBinding(self.program(), blockIndex, bindPoint)
### create a buffer
#buf = glGenBuffers(1)
#glBindBuffer(GL_UNIFORM_BUFFER, buf)
#glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW)
### also possible to use glBufferSubData to fill parts of the buffer
### bind buffer to the same binding point
#glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf)
except:
glUseProgram(0)
raise
def __exit__(self, *args): def __exit__(self, *args):
glUseProgram(0) if len(self.shaders) > 0:
glUseProgram(0)
def uniform(self, name): def uniform(self, name):
"""Return the location integer for a uniform variable in this program""" """Return the location integer for a uniform variable in this program"""
return glGetUniformLocation(self.program(), name) return glGetUniformLocation(self.program(), name)
#def uniformBlockInfo(self, blockName):
#blockIndex = glGetUniformBlockIndex(self.program(), blockName)
#count = glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS)
#indices = []
#for i in range(count):
#indices.append(glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES))
class HeightColorShader(ShaderProgram):
def __enter__(self):
## Program should have a uniform block declared:
##
## layout (std140) uniform blockName {
## vec4 diffuse;
## vec4 ambient;
## };
## pick any-old binding point. (there are a limited number of these per-program
bindPoint = 1
## get the block index for a uniform variable in the shader
blockIndex = glGetUniformBlockIndex(self.program(), "blockName")
## give the shader block a binding point
glUniformBlockBinding(self.program(), blockIndex, bindPoint)
## create a buffer
buf = glGenBuffers(1)
glBindBuffer(GL_UNIFORM_BUFFER, buf)
glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW)
## also possible to use glBufferSubData to fill parts of the buffer
## bind buffer to the same binding point
glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf)
initShaders() initShaders()

View File

@ -460,7 +460,10 @@ class Parameter(QtCore.QObject):
self.childs.pop(self.childs.index(child)) self.childs.pop(self.childs.index(child))
child.parentChanged(None) child.parentChanged(None)
self.sigChildRemoved.emit(self, child) self.sigChildRemoved.emit(self, child)
child.sigTreeStateChanged.disconnect(self.treeStateChanged) try:
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
except TypeError: ## already disconnected
pass
def clearChildren(self): def clearChildren(self):
"""Remove all child parameters.""" """Remove all child parameters."""
@ -550,6 +553,8 @@ class Parameter(QtCore.QObject):
def __getattr__(self, attr): def __getattr__(self, attr):
## Leaving this undocumented because I might like to remove it in the future.. ## Leaving this undocumented because I might like to remove it in the future..
#print type(self), attr #print type(self), attr
if 'names' not in self.__dict__:
raise AttributeError(attr)
if attr in self.names: if attr in self.names:
return self.param(attr) return self.param(attr)
else: else:

View File

@ -56,8 +56,13 @@ class LayoutWidget(QtGui.QWidget):
""" """
Add a widget to the layout and place it in the next available cell (or in the cell specified). Add a widget to the layout and place it in the next available cell (or in the cell specified).
""" """
if row is None: if row == 'next':
self.nextRow()
row = self.currentRow row = self.currentRow
elif row is None:
row = self.currentRow
if col is None: if col is None:
col = self.nextCol(colspan) col = self.nextCol(colspan)