From b09182d19af815976917dba95db19bd0010f2efa Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 26 Oct 2012 21:47:45 -0400 Subject: [PATCH 1/5] GLScatterPlotItem: use shader programs to allow specifying spot size by array Reorganized shader programs Infrastructure updates for OpenGL system --- SRTTransform3D.py | 12 +-- Transform3D.py | 35 ++++++++ Vector.py | 123 ++++++++++++++------------- __init__.py | 1 + examples/GLScatterPlotItem.py | 93 +++++++++++++++------ opengl/GLGraphicsItem.py | 44 ++++++++-- opengl/GLViewWidget.py | 12 ++- opengl/items/GLMeshItem.py | 10 ++- opengl/items/GLScatterPlotItem.py | 130 +++++++++++++++++++---------- opengl/shaders.py | 134 ++++++++++++++++++++++-------- 10 files changed, 411 insertions(+), 183 deletions(-) create mode 100644 Transform3D.py diff --git a/SRTTransform3D.py b/SRTTransform3D.py index 94c3df77..89b8ab13 100644 --- a/SRTTransform3D.py +++ b/SRTTransform3D.py @@ -6,12 +6,12 @@ import pyqtgraph as pg import numpy as np import scipy.linalg -class SRTTransform3D(QtGui.QMatrix4x4): +class SRTTransform3D(pg.Transform3D): """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. """ def __init__(self, init=None): - QtGui.QMatrix4x4.__init__(self) + pg.Transform3D.__init__(self) self.reset() if init is None: return @@ -190,11 +190,11 @@ class SRTTransform3D(QtGui.QMatrix4x4): self.update() def update(self): - QtGui.QMatrix4x4.setToIdentity(self) + pg.Transform3D.setToIdentity(self) ## modifications to the transform are multiplied on the right, so we need to reverse order here. - QtGui.QMatrix4x4.translate(self, *self._state['pos']) - QtGui.QMatrix4x4.rotate(self, self._state['angle'], *self._state['axis']) - QtGui.QMatrix4x4.scale(self, *self._state['scale']) + pg.Transform3D.translate(self, *self._state['pos']) + pg.Transform3D.rotate(self, self._state['angle'], *self._state['axis']) + pg.Transform3D.scale(self, *self._state['scale']) def __repr__(self): return str(self.saveState()) diff --git a/Transform3D.py b/Transform3D.py new file mode 100644 index 00000000..aa948e28 --- /dev/null +++ b/Transform3D.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from .Qt import QtCore, QtGui +import pyqtgraph as pg +import numpy as np + +class Transform3D(QtGui.QMatrix4x4): + """ + Extension of QMatrix4x4 with some helpful methods added. + """ + def __init__(self, *args): + QtGui.QMatrix4x4.__init__(self, *args) + + def matrix(self, nd=3): + if nd == 3: + return np.array(self.copyDataTo()).reshape(4,4) + elif nd == 2: + m = np.array(self.copyDataTo()).reshape(4,4) + m[2] = m[3] + m[:,2] = m[:,3] + return m[:3,:3] + else: + raise Exception("Argument 'nd' must be 2 or 3") + + def map(self, obj): + """ + Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates + """ + if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): + return pg.transformCoordinates(self, obj) + else: + return QtGui.QMatrix4x4.map(self, obj) + + def inverted(self): + inv, b = QtGui.QMatrix4x4.inverted(self) + return Transform3D(inv), b \ No newline at end of file diff --git a/Vector.py b/Vector.py index 79da3162..e9c109d8 100644 --- a/Vector.py +++ b/Vector.py @@ -1,59 +1,64 @@ -# -*- coding: utf-8 -*- -""" -Vector.py - Extension of QVector3D which adds a few missing methods. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. -""" - -from .Qt import QtGui, QtCore -import numpy as np - -class Vector(QtGui.QVector3D): - """Extension of QVector3D which adds a few helpful methods.""" - - def __init__(self, *args): - if len(args) == 1: - if isinstance(args[0], QtCore.QSizeF): - QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0) - return - elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF): - QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0) - elif hasattr(args[0], '__getitem__'): - vals = list(args[0]) - if len(vals) == 2: - vals.append(0) - if len(vals) != 3: - raise Exception('Cannot init Vector with sequence of length %d' % len(args[0])) - QtGui.QVector3D.__init__(self, *vals) - return - elif len(args) == 2: - QtGui.QVector3D.__init__(self, args[0], args[1], 0) - return - QtGui.QVector3D.__init__(self, *args) - - def __len__(self): - return 3 - - #def __reduce__(self): - #return (Point, (self.x(), self.y())) - - def __getitem__(self, i): - if i == 0: - return self.x() - elif i == 1: - return self.y() - elif i == 2: - return self.z() - else: - raise IndexError("Point has no index %s" % str(i)) - - def __setitem__(self, i, x): - if i == 0: - return self.setX(x) - elif i == 1: - return self.setY(x) - elif i == 2: - return self.setZ(x) - else: - raise IndexError("Point has no index %s" % str(i)) - +# -*- coding: utf-8 -*- +""" +Vector.py - Extension of QVector3D which adds a few missing methods. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from .Qt import QtGui, QtCore +import numpy as np + +class Vector(QtGui.QVector3D): + """Extension of QVector3D which adds a few helpful methods.""" + + def __init__(self, *args): + if len(args) == 1: + if isinstance(args[0], QtCore.QSizeF): + QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0) + return + elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF): + QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0) + elif hasattr(args[0], '__getitem__'): + vals = list(args[0]) + if len(vals) == 2: + vals.append(0) + if len(vals) != 3: + raise Exception('Cannot init Vector with sequence of length %d' % len(args[0])) + QtGui.QVector3D.__init__(self, *vals) + return + elif len(args) == 2: + QtGui.QVector3D.__init__(self, args[0], args[1], 0) + return + QtGui.QVector3D.__init__(self, *args) + + def __len__(self): + return 3 + + #def __reduce__(self): + #return (Point, (self.x(), self.y())) + + def __getitem__(self, i): + if i == 0: + return self.x() + elif i == 1: + return self.y() + elif i == 2: + return self.z() + else: + raise IndexError("Point has no index %s" % str(i)) + + def __setitem__(self, i, x): + if i == 0: + return self.setX(x) + elif i == 1: + return self.setY(x) + elif i == 2: + return self.setZ(x) + else: + raise IndexError("Point has no index %s" % str(i)) + + def __iter__(self): + yield(self.x()) + yield(self.y()) + yield(self.z()) + \ No newline at end of file diff --git a/__init__.py b/__init__.py index bd7c2e76..dbb54ca9 100644 --- a/__init__.py +++ b/__init__.py @@ -165,6 +165,7 @@ from .WidgetGroup import * from .Point import Point from .Vector import Vector from .SRTTransform import SRTTransform +from .Transform3D import Transform3D from .SRTTransform3D import SRTTransform3D from .functions import * from .graphicsWindows import * diff --git a/examples/GLScatterPlotItem.py b/examples/GLScatterPlotItem.py index 16033520..e73eacd9 100644 --- a/examples/GLScatterPlotItem.py +++ b/examples/GLScatterPlotItem.py @@ -15,48 +15,91 @@ w.show() g = gl.GLGridItem() w.addItem(g) -#pos = np.empty((53, 3)) -#size = np.empty((53)) -#color = np.empty((53, 4)) -#pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5) -#pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5) -#pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5) -#z = 0.5 -#d = 6.0 -#for i in range(3,53): - #pos[i] = (0,0,z) - #size[i] = 2./d - #color[i] = (0.0, 1.0, 0.0, 0.5) - #z *= 0.5 - #d *= 2.0 +## +## First example is a set of points with pxMode=False +## These demonstrate the ability to have points with real size down to a very small scale +## +pos = np.empty((53, 3)) +size = np.empty((53)) +color = np.empty((53, 4)) +pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5) +pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5) +pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5) + +z = 0.5 +d = 6.0 +for i in range(3,53): + pos[i] = (0,0,z) + size[i] = 2./d + color[i] = (0.0, 1.0, 0.0, 0.5) + z *= 0.5 + d *= 2.0 -#sp = gl.GLScatterPlotItem(pos=pos, sizes=size, colors=color, pxMode=False) +sp1 = gl.GLScatterPlotItem(pos=pos, size=size, color=color, pxMode=False) +sp1.translate(5,5,0) +w.addItem(sp1) -pos = (np.random.random(size=(100000,3)) * 10) - 5 +## +## Second example shows a volume of points with rapidly updating color +## and pxMode=True +## + +pos = np.random.random(size=(100000,3)) +pos *= [10,-10,10] +pos[0] = (0,0,0) color = np.ones((pos.shape[0], 4)) -d = (pos**2).sum(axis=1)**0.5 -color[:,3] = np.clip(-np.cos(d*2) * 0.2, 0, 1) -sp = gl.GLScatterPlotItem(pos=pos, color=color, size=5) +d2 = (pos**2).sum(axis=1)**0.5 +size = np.random.random(size=pos.shape[0])*10 +sp2 = gl.GLScatterPlotItem(pos=pos, color=(1,1,1,1), size=size) phase = 0. +w.addItem(sp2) + + +## +## Third example shows a grid of points with rapidly updating position +## and pxMode = False +## + +pos3 = np.zeros((100,100,3)) +pos3[:,:,:2] = np.mgrid[:100, :100].transpose(1,2,0) * [-0.1,0.1] +pos3 = pos3.reshape(10000,3) +d3 = (pos3**2).sum(axis=1)**0.5 + +sp3 = gl.GLScatterPlotItem(pos=pos3, color=(1,1,1,.3), size=0.1, pxMode=False) + +w.addItem(sp3) + + def update(): - global phase, color, sp, d - s = -np.cos(d*2+phase) - color[:,3] = np.clip(s * 0.2, 0, 1) + ## update volume colors + global phase, sp2, d2 + s = -np.cos(d2*2+phase) + color = np.empty((len(d2),4), dtype=np.float32) + color[:,3] = np.clip(s * 0.1, 0, 1) color[:,0] = np.clip(s * 3.0, 0, 1) color[:,1] = np.clip(s * 1.0, 0, 1) color[:,2] = np.clip(s ** 3, 0, 1) - - sp.setData(color=color) + sp2.setData(color=color) phase -= 0.1 + ## update surface positions and colors + global sp3, d3, pos3 + z = -np.cos(d3*2+phase) + pos3[:,2] = z + color = np.empty((len(d3),4), dtype=np.float32) + color[:,3] = 0.3 + color[:,0] = np.clip(z * 3.0, 0, 1) + color[:,1] = np.clip(z * 1.0, 0, 1) + color[:,2] = np.clip(z ** 3, 0, 1) + sp3.setData(pos=pos3, color=color) + t = QtCore.QTimer() t.timeout.connect(update) t.start(50) -w.addItem(sp) ## Start Qt event loop unless running in interactive mode. if sys.flags.interactive != 1: diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py index 96cc6763..7d1cf70b 100644 --- a/opengl/GLGraphicsItem.py +++ b/opengl/GLGraphicsItem.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph import Transform3D class GLGraphicsItem(QtCore.QObject): def __init__(self, parentItem=None): @@ -6,7 +7,7 @@ class GLGraphicsItem(QtCore.QObject): self.__parent = None self.__view = None self.__children = set() - self.__transform = QtGui.QMatrix4x4() + self.__transform = Transform3D() self.__visible = True self.setParentItem(parentItem) self.setDepthValue(0) @@ -50,7 +51,7 @@ class GLGraphicsItem(QtCore.QObject): return self.__depthValue def setTransform(self, tr): - self.__transform = tr + self.__transform = Transform3D(tr) self.update() def resetTransform(self): @@ -73,12 +74,22 @@ class GLGraphicsItem(QtCore.QObject): def transform(self): return self.__transform + def viewTransform(self): + tr = self.__transform + p = self + while True: + p = p.parentItem() + if p is None: + break + tr = p.transform() * tr + return Transform3D(tr) + def translate(self, dx, dy, dz, local=False): """ Translate the object by (*dx*, *dy*, *dz*) in its parent's coordinate system. If *local* is True, then translation takes place in local coordinates. """ - tr = QtGui.QMatrix4x4() + tr = Transform3D() tr.translate(dx, dy, dz) self.applyTransform(tr, local=local) @@ -88,7 +99,7 @@ class GLGraphicsItem(QtCore.QObject): *angle* is in degrees. """ - tr = QtGui.QMatrix4x4() + tr = Transform3D() tr.rotate(angle, x, y, z) self.applyTransform(tr, local=local) @@ -97,7 +108,7 @@ class GLGraphicsItem(QtCore.QObject): Scale the object by (*dx*, *dy*, *dz*) in its local coordinate system. If *local* is False, then scale takes place in the parent's coordinates. """ - tr = QtGui.QMatrix4x4() + tr = Transform3D() tr.scale(x, y, z) self.applyTransform(tr, local=local) @@ -138,8 +149,29 @@ class GLGraphicsItem(QtCore.QObject): return v.updateGL() + def mapToParent(self, point): + tr = self.transform() + if tr is None: + return point + return tr.map(point) + def mapFromParent(self, point): tr = self.transform() if tr is None: return point - return tr.inverted()[0].map(point) \ No newline at end of file + return tr.inverted()[0].map(point) + + def mapToView(self, point): + tr = self.viewTransform() + if tr is None: + return point + return tr.map(point) + + def mapFromView(self, point): + tr = self.viewTransform() + if tr is None: + return point + return tr.inverted()[0].map(point) + + + \ No newline at end of file diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py index 3e105491..6911d849 100644 --- a/opengl/GLViewWidget.py +++ b/opengl/GLViewWidget.py @@ -1,8 +1,8 @@ from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL from OpenGL.GL import * import numpy as np - -Vector = QtGui.QVector3D +from pyqtgraph import Vector +##Vector = QtGui.QVector3D class GLViewWidget(QtOpenGL.QGLWidget): """ @@ -181,10 +181,14 @@ class GLViewWidget(QtOpenGL.QGLWidget): def pixelSize(self, pos): """ Return the approximate size of a screen pixel at the location pos - + Pos may be a Vector or an (N,3) array of locations """ cam = self.cameraPosition() - dist = (pos-cam).length() + if isinstance(pos, np.ndarray) and pos.ndim == 2: + cam = np.array(cam).reshape(1,3) + dist = ((pos-cam)**2).sum(axis=1)**0.5 + else: + dist = (pos-cam).length() xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) return xDist / self.width() diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py index 790c6760..266b84c0 100644 --- a/opengl/items/GLMeshItem.py +++ b/opengl/items/GLMeshItem.py @@ -28,7 +28,7 @@ class GLMeshItem(GLGraphicsItem): GLGraphicsItem.__init__(self) def initializeGL(self): - self.shader = shaders.getShader('balloon') + self.shader = shaders.getShaderProgram('balloon') l = glGenLists(1) self.triList = l @@ -72,7 +72,9 @@ class GLMeshItem(GLGraphicsItem): def paint(self): - shaders.glUseProgram(self.shader) - glCallList(self.triList) - shaders.glUseProgram(0) + with self.shader: + glCallList(self.triList) + #shaders.glUseProgram(self.shader) + #glCallList(self.triList) + #shaders.glUseProgram(0) #glCallList(self.meshList) diff --git a/opengl/items/GLScatterPlotItem.py b/opengl/items/GLScatterPlotItem.py index 3ef3f11b..1134ce51 100644 --- a/opengl/items/GLScatterPlotItem.py +++ b/opengl/items/GLScatterPlotItem.py @@ -1,5 +1,7 @@ from OpenGL.GL import * +from OpenGL.arrays import vbo from .. GLGraphicsItem import GLGraphicsItem +from .. import shaders from pyqtgraph import QtGui import numpy as np @@ -14,6 +16,7 @@ class GLScatterPlotItem(GLGraphicsItem): self.size = 10 self.color = [1.0,1.0,1.0,0.5] self.pxMode = True + #self.vbo = {} ## VBO does not appear to improve performance very much. self.setData(**kwds) def setData(self, **kwds): @@ -39,13 +42,16 @@ class GLScatterPlotItem(GLGraphicsItem): for k in kwds.keys(): if k not in args: raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) - self.pos = kwds.get('pos', self.pos) - self.color = kwds.get('color', self.color) - self.size = kwds.get('size', self.size) + + args.remove('pxMode') + for arg in args: + if arg in kwds: + setattr(self, arg, kwds[arg]) + #self.vbo.pop(arg, None) + self.pxMode = kwds.get('pxMode', self.pxMode) self.update() - def initializeGL(self): ## Generate texture for rendering points @@ -65,73 +71,105 @@ class GLScatterPlotItem(GLGraphicsItem): 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) - def paint(self): - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + self.shader = shaders.getShaderProgram('point_sprite') + + #def getVBO(self, name): + #if name not in self.vbo: + #self.vbo[name] = vbo.VBO(getattr(self, name).astype('f')) + #return self.vbo[name] + + def setupGLState(self): + """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) glEnable( GL_BLEND ) glEnable( GL_ALPHA_TEST ) - glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + + #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_SIZE_MAX, (65500,)) #glPointParameterfv(GL_POINT_SIZE_MIN, (0,)) + def paint(self): + self.setupGLState() + glEnable(GL_POINT_SPRITE) + glActiveTexture(GL_TEXTURE0) glEnable( GL_TEXTURE_2D ) glBindTexture(GL_TEXTURE_2D, self.pointTexture) glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE) #glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) ## use texture color exactly - glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ) ## texture modulates current color + #glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ) ## texture modulates current color glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glEnable(GL_PROGRAM_POINT_SIZE) - if self.pxMode: - glVertexPointerf(self.pos) - if isinstance(self.color, np.ndarray): - glColorPointerf(self.color) - else: - if isinstance(self.color, QtGui.QColor): - glColor4f(*fn.glColor(self.color)) - else: - glColor4f(*self.color) - if isinstance(self.size, np.ndarray): - raise Exception('Array size not yet supported in pxMode (hopefully soon)') - - glPointSize(self.size) + with self.shader: + #glUniform1i(self.shader.uniform('texture'), 0) ## inform the shader which texture to use glEnableClientState(GL_VERTEX_ARRAY) - glEnableClientState(GL_COLOR_ARRAY) - glDrawArrays(GL_POINTS, 0, len(self.pos)) - else: + try: + glVertexPointerf(self.pos) - - for i in range(len(self.pos)): - pos = self.pos[i] - if isinstance(self.color, np.ndarray): - color = self.color[i] + glEnableClientState(GL_COLOR_ARRAY) + glColorPointerf(self.color) else: - color = self.color - if isinstance(self.color, QtGui.QColor): - color = fn.glColor(self.color) - - if isinstance(self.size, np.ndarray): - size = self.size[i] - else: - size = self.size - - pxSize = self.view().pixelSize(QtGui.QVector3D(*pos)) + if isinstance(self.color, QtGui.QColor): + glColor4f(*fn.glColor(self.color)) + else: + glColor4f(*self.color) - glPointSize(size / pxSize) - glBegin( GL_POINTS ) - glColor4f(*color) # x is blue - #glNormal3f(size, 0, 0) - glVertex3f(*pos) - glEnd() + if not self.pxMode or isinstance(self.size, np.ndarray): + glEnableClientState(GL_NORMAL_ARRAY) + norm = np.empty(self.pos.shape) + if self.pxMode: + norm[:,0] = self.size + else: + gpos = self.mapToView(self.pos.transpose()).transpose() + pxSize = self.view().pixelSize(gpos) + norm[:,0] = self.size / pxSize + + glNormalPointerf(norm) + else: + glPointSize(self.size) + glDrawArrays(GL_POINTS, 0, len(self.pos)) + finally: + glDisableClientState(GL_NORMAL_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + #posVBO.unbind() + + #for i in range(len(self.pos)): + #pos = self.pos[i] + + #if isinstance(self.color, np.ndarray): + #color = self.color[i] + #else: + #color = self.color + #if isinstance(self.color, QtGui.QColor): + #color = fn.glColor(self.color) + + #if isinstance(self.size, np.ndarray): + #size = self.size[i] + #else: + #size = self.size + + #pxSize = self.view().pixelSize(QtGui.QVector3D(*pos)) + + #glPointSize(size / pxSize) + #glBegin( GL_POINTS ) + #glColor4f(*color) # x is blue + ##glNormal3f(size, 0, 0) + #glVertex3f(*pos) + #glEnd() diff --git a/opengl/shaders.py b/opengl/shaders.py index b1216e35..7f4fa665 100644 --- a/opengl/shaders.py +++ b/opengl/shaders.py @@ -3,39 +3,107 @@ from OpenGL.GL import shaders ## For centralizing and managing vertex/fragment shader programs. +def initShaders(): + global Shaders + Shaders = [ + ShaderProgram('balloon', [ ## increases fragment alpha as the normal turns orthogonal to the view + VertexShader(""" + varying vec3 normal; + void main() { + 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_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + gl_FragColor = color; + } + """) + ]), + ShaderProgram('point_sprite', [ ## allows specifying point size using normal.x + ## 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/3497068/textured-points-in-opengl-es-2-0 + ## + ## + VertexShader(""" + void main() { + gl_FrontColor=gl_Color; + gl_PointSize = gl_Normal.x; + gl_Position = ftransform(); + } + """), + #FragmentShader(""" + ##version 120 + #uniform sampler2D texture; + #void main ( ) + #{ + #gl_FragColor = texture2D(texture, gl_PointCoord) * gl_Color; + #} + #""") + ]), + ] -Shaders = { - 'balloon': ( ## increases fragment alpha as the normal turns orthogonal to the view - """ - varying vec3 normal; - void main() { - 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_BackColor = gl_Color; - gl_Position = ftransform(); - } - """, - """ - varying vec3 normal; - void main() { - vec4 color = gl_Color; - color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); - gl_FragColor = color; - } - """ - ), -} -CompiledShaders = {} + +CompiledShaderPrograms = {} -def getShader(name): - global Shaders, CompiledShaders +def getShaderProgram(name): + return ShaderProgram.names[name] + +class VertexShader: + def __init__(self, code): + self.code = code + self.compiled = None + + def shader(self): + if self.compiled is None: + self.compiled = shaders.compileShader(self.code, GL_VERTEX_SHADER) + return self.compiled + +class FragmentShader: + def __init__(self, code): + self.code = code + self.compiled = None + + def shader(self): + if self.compiled is None: + self.compiled = shaders.compileShader(self.code, GL_FRAGMENT_SHADER) + return self.compiled + + + +class ShaderProgram: + names = {} - if name not in CompiledShaders: - vshader, fshader = Shaders[name] - vcomp = shaders.compileShader(vshader, GL_VERTEX_SHADER) - fcomp = shaders.compileShader(fshader, GL_FRAGMENT_SHADER) - prog = shaders.compileProgram(vcomp, fcomp) - CompiledShaders[name] = prog, vcomp, fcomp - return CompiledShaders[name][0] + def __init__(self, name, shaders): + self.name = name + ShaderProgram.names[name] = self + self.shaders = shaders + self.prog = None + + def program(self): + if self.prog is None: + compiled = [s.shader() for s in self.shaders] ## compile all shaders + self.prog = shaders.compileProgram(*compiled) ## compile program + return self.prog + + def __enter__(self): + glUseProgram(self.program()) + + def __exit__(self, *args): + glUseProgram(0) + + def uniform(self, name): + """Return the location integer for a uniform variable in this program""" + return glGetUniformLocation(self.program(), name) + + +initShaders() \ No newline at end of file From 679de86509df646e5144a784dfb4b02dd8c2222d Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 31 Oct 2012 01:53:16 -0400 Subject: [PATCH 2/5] Minor changes and fixes: - documentation updates - PlotItem informs all items when switching to log mode - GradientEditorItem has sigGradientChangeFinished - ParameterTree list types check linits on initialization - Fixed RuntimeError in TreeWidget.clear() --- dockarea/DockArea.py | 13 ++++++--- graphicsItems/GradientEditorItem.py | 42 ++++++++++++++++++++++------- graphicsItems/PlotDataItem.py | 2 ++ graphicsItems/PlotItem/PlotItem.py | 10 ++++--- parametertree/parameterTypes.py | 1 + widgets/GradientWidget.py | 2 ++ widgets/TreeWidget.py | 6 +++-- 7 files changed, 59 insertions(+), 17 deletions(-) diff --git a/dockarea/DockArea.py b/dockarea/DockArea.py index d49f02ad..78d512f3 100644 --- a/dockarea/DockArea.py +++ b/dockarea/DockArea.py @@ -208,6 +208,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def restoreState(self, state): """ Restore Dock configuration as generated by saveState. + + Note that this function does not create any Docks--it will only + restore the arrangement of an existing set of Docks. + """ ## 1) make dict of all docks and list of existing containers @@ -240,8 +244,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): typ, contents, state = state pfx = " " * depth if typ == 'dock': - obj = docks[contents] - del docks[contents] + try: + obj = docks[contents] + del docks[contents] + except KeyError: + raise Exception('Cannot restore dock state; no dock with name "%s"' % contents) else: obj = self.makeContainer(typ) @@ -270,7 +277,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): if isinstance(obj, Dock): d[obj.name()] = obj - else: + elif obj is not None: c.append(obj) for i in range(obj.count()): o2 = obj.widget(i) diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py index 33970d2c..3c078ede 100644 --- a/graphicsItems/GradientEditorItem.py +++ b/graphicsItems/GradientEditorItem.py @@ -165,6 +165,9 @@ class TickSliderItem(GraphicsWidget): tick.setPos(pos) self.ticks[tick] = float(newX) / self.length + def tickMoveFinished(self, tick): + pass + def tickClicked(self, tick, ev): if ev.button() == QtCore.Qt.RightButton: self.removeTick(tick) @@ -340,16 +343,18 @@ class GradientEditorItem(TickSliderItem): customizable by the user. :class: `GradientWidget ` provides a widget with a GradientEditorItem that can be added to a GUI. - ======================== =========================================================== + ================================ =========================================================== **Signals** - sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal - is emitted in real time while ticks are being dragged or - colors are being changed. - ======================== =========================================================== + sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal + is emitted in real time while ticks are being dragged or + colors are being changed. + sigGradientChangeFinished(self) Signal is emitted when the gradient is finished changing. + ================================ =========================================================== """ sigGradientChanged = QtCore.Signal(object) + sigGradientChangeFinished = QtCore.Signal(object) def __init__(self, *args, **kargs): """ @@ -381,6 +386,7 @@ class GradientEditorItem(TickSliderItem): self.colorDialog.currentColorChanged.connect(self.currentColorChanged) self.colorDialog.rejected.connect(self.currentColorRejected) + self.colorDialog.accepted.connect(self.currentColorAccepted) self.backgroundRect.setParentItem(self) self.gradRect.setParentItem(self) @@ -508,6 +514,9 @@ class GradientEditorItem(TickSliderItem): self.setTickColor(self.currentTick, self.currentTickColor) self.updateGradient() + def currentColorAccepted(self): + self.sigGradientChangeFinished.emit(self) + def tickClicked(self, tick, ev): #private if ev.button() == QtCore.Qt.LeftButton: @@ -533,6 +542,9 @@ class GradientEditorItem(TickSliderItem): TickSliderItem.tickMoved(self, tick, pos) self.updateGradient() + def tickMoveFinished(self, tick): + self.sigGradientChangeFinished.emit(self) + def getGradient(self): """Return a QLinearGradient object.""" @@ -669,7 +681,7 @@ class GradientEditorItem(TickSliderItem): TickSliderItem.mouseReleaseEvent(self, ev) self.updateGradient() - def addTick(self, x, color=None, movable=True): + def addTick(self, x, color=None, movable=True, finish=True): """ Add a tick to the gradient. Return the tick. @@ -688,7 +700,17 @@ class GradientEditorItem(TickSliderItem): t = TickSliderItem.addTick(self, x, color=color, movable=movable) t.colorChangeAllowed = True t.removeAllowed = True + + if finish: + self.sigGradientChangeFinished.emit(self) return t + + + def removeTick(self, tick, finish=True): + TickSliderItem.removeTick(self, tick) + if finish: + self.sigGradientChangeFinished.emit(self) + def saveState(self): """ @@ -723,13 +745,14 @@ class GradientEditorItem(TickSliderItem): ## public self.setColorMode(state['mode']) for t in list(self.ticks.keys()): - self.removeTick(t) + self.removeTick(t, finish=False) for t in state['ticks']: c = QtGui.QColor(*t[1]) - self.addTick(t[0], c) + self.addTick(t[0], c, finish=False) self.updateGradient() + self.sigGradientChangeFinished.emit(self) - + class Tick(GraphicsObject): ## private class @@ -791,6 +814,7 @@ class Tick(GraphicsObject): if ev.isFinish(): self.moving = False self.sigMoved.emit(self) + self.view().tickMoveFinished(self) def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.moving: diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 34af641a..ce5b11aa 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -361,9 +361,11 @@ class PlotDataItem(GraphicsObject): self.updateItems() prof.mark('update items') + view = self.getViewBox() if view is not None: view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + self.sigPlotChanged.emit(self) prof.mark('emit') prof.finish() diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 46bc4d5b..3177a176 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -506,12 +506,14 @@ class PlotItem(GraphicsWidget): self.curves.append(item) #self.addItem(c) + if hasattr(item, 'setLogMode'): + item.setLogMode(self.ctrl.logXCheck.isChecked(), self.ctrl.logYCheck.isChecked()) + if isinstance(item, PlotDataItem): ## configure curve for this plot (alpha, auto) = self.alphaState() item.setAlpha(alpha, auto) item.setFftMode(self.ctrl.fftCheck.isChecked()) - item.setLogMode(self.ctrl.logXCheck.isChecked(), self.ctrl.logYCheck.isChecked()) item.setDownsampling(self.downsampleMode()) item.setPointMode(self.pointMode()) @@ -526,6 +528,7 @@ class PlotItem(GraphicsWidget): #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged) #self.plotChanged() + def addDataItem(self, item, *args): print("PlotItem.addDataItem is deprecated. Use addItem instead.") @@ -878,8 +881,9 @@ class PlotItem(GraphicsWidget): def updateLogMode(self): x = self.ctrl.logXCheck.isChecked() y = self.ctrl.logYCheck.isChecked() - for c in self.curves: - c.setLogMode(x,y) + for i in self.items: + if hasattr(i, 'setLogMode'): + i.setLogMode(x,y) self.getAxis('bottom').setLogMode(x) self.getAxis('top').setLogMode(x) self.getAxis('left').setLogMode(y) diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py index 315081ce..3aab5a6d 100644 --- a/parametertree/parameterTypes.py +++ b/parametertree/parameterTypes.py @@ -516,6 +516,7 @@ class ListParameter(Parameter): if opts.get('limits', None) is None: opts['limits'] = [] Parameter.__init__(self, **opts) + self.setLimits(opts['limits']) def setLimits(self, limits): self.forward, self.reverse = self.mapping(limits) diff --git a/widgets/GradientWidget.py b/widgets/GradientWidget.py index ecf3f585..2b9b52d2 100644 --- a/widgets/GradientWidget.py +++ b/widgets/GradientWidget.py @@ -11,6 +11,7 @@ __all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider'] class GradientWidget(GraphicsView): sigGradientChanged = QtCore.Signal(object) + sigGradientChangeFinished = QtCore.Signal(object) def __init__(self, parent=None, orientation='bottom', *args, **kargs): GraphicsView.__init__(self, parent, useOpenGL=False, background=None) @@ -18,6 +19,7 @@ class GradientWidget(GraphicsView): kargs['tickPen'] = 'k' self.item = GradientEditorItem(*args, **kargs) self.item.sigGradientChanged.connect(self.sigGradientChanged) + self.item.sigGradientChangeFinished.connect(self.sigGradientChangeFinished) self.setCentralItem(self.item) self.setOrientation(orientation) self.setCacheMode(self.CacheNone) diff --git a/widgets/TreeWidget.py b/widgets/TreeWidget.py index ef2f1ec9..97fbe953 100644 --- a/widgets/TreeWidget.py +++ b/widgets/TreeWidget.py @@ -201,8 +201,10 @@ class TreeWidget(QtGui.QTreeWidget): for item in items: self.prepareMove(item) QtGui.QTreeWidget.clear(self) - for item in items: - self.informTreeWidgetChange(item) + + ## Why do we want to do this? It causes RuntimeErrors. + #for item in items: + #self.informTreeWidgetChange(item) class TreeWidgetItem(QtGui.QTreeWidgetItem): From 2c679dfbcc910b9ad3664ef97454fd4d2db18fca Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 31 Oct 2012 01:57:00 -0400 Subject: [PATCH 3/5] ViewBox will now correctly auto-range when an item's position or transform changes. --- graphicsItems/GraphicsItem.py | 16 +++- graphicsItems/GraphicsObject.py | 3 + graphicsItems/ViewBox/ViewBox.py | 143 +++++++++++++++++-------------- 3 files changed, 96 insertions(+), 66 deletions(-) diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py index 347d3886..7eee89f3 100644 --- a/graphicsItems/GraphicsItem.py +++ b/graphicsItems/GraphicsItem.py @@ -433,4 +433,18 @@ class GraphicsItem(object): """ Called whenever the transformation matrix of the view has changed. """ - pass \ No newline at end of file + pass + + #def prepareGeometryChange(self): + #self._qtBaseClass.prepareGeometryChange(self) + #self.informViewBoundsChanged() + + def informViewBoundsChanged(self): + """ + Inform this item's container ViewBox that the bounds of this item have changed. + This is used by ViewBox to react if auto-range is enabled. + """ + view = self.getViewBox() + if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'): + view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + \ No newline at end of file diff --git a/graphicsItems/GraphicsObject.py b/graphicsItems/GraphicsObject.py index f893d8dc..4361d1e6 100644 --- a/graphicsItems/GraphicsObject.py +++ b/graphicsItems/GraphicsObject.py @@ -11,10 +11,13 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): _qtBaseClass = QtGui.QGraphicsObject def __init__(self, *args): QtGui.QGraphicsObject.__init__(self, *args) + self.setFlag(self.ItemSendsGeometryChanges) GraphicsItem.__init__(self) def itemChange(self, change, value): ret = QtGui.QGraphicsObject.itemChange(self, change, value) if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: self._updateView() + if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: + self.informViewBoundsChanged() return ret diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index d403291b..89fa1487 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -109,6 +109,7 @@ class ViewBox(GraphicsWidget): 'background': None, } + self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self.setFlag(self.ItemClipsChildrenToShape) @@ -340,6 +341,7 @@ class ViewBox(GraphicsWidget): ============= ===================================================================== """ + changes = {} if rect is not None: @@ -471,6 +473,7 @@ class ViewBox(GraphicsWidget): #if not enable: #import traceback #traceback.print_stack() + if enable is True: enable = 1.0 @@ -520,74 +523,84 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() def updateAutoRange(self): - targetRect = self.viewRange() - if not any(self.state['autoRange']): + ## Break recursive loops when auto-ranging. + ## This is needed because some items change their size in response + ## to a view change. + if self._updatingRange: return - - fractionVisible = self.state['autoRange'][:] - for i in [0,1]: - if type(fractionVisible[i]) is bool: - fractionVisible[i] = 1.0 - - childRange = None - - order = [0,1] - if self.state['autoVisibleOnly'][0] is True: - order = [1,0] - - args = {} - for ax in order: - if self.state['autoRange'][ax] is False: - continue - if self.state['autoVisibleOnly'][ax]: - oRange = [None, None] - oRange[ax] = targetRect[1-ax] - childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange) + + self._updatingRange = True + try: + targetRect = self.viewRange() + if not any(self.state['autoRange']): + return - else: - if childRange is None: - childRange = self.childrenBounds(frac=fractionVisible) - - ## Make corrections to range - xr = childRange[ax] - if xr is not None: - if self.state['autoPan'][ax]: - x = sum(xr) * 0.5 - #x = childRect.center().x() - w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. - #childRect.setLeft(x-w2) - #childRect.setRight(x+w2) - childRange[ax] = [x-w2, x+w2] - else: - #wp = childRect.width() * 0.02 - wp = (xr[1] - xr[0]) * 0.02 - #childRect = childRect.adjusted(-wp, 0, wp, 0) - childRange[ax][0] -= wp - childRange[ax][1] += wp - #targetRect[ax][0] = childRect.left() - #targetRect[ax][1] = childRect.right() - targetRect[ax] = childRange[ax] - args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] - #else: - ### Make corrections to Y range - #if self.state['autoPan'][1]: - #y = childRect.center().y() - #h2 = (targetRect[1][1]-targetRect[1][0]) / 2. - #childRect.setTop(y-h2) - #childRect.setBottom(y+h2) - #else: - #hp = childRect.height() * 0.02 - #childRect = childRect.adjusted(0, -hp, 0, hp) + fractionVisible = self.state['autoRange'][:] + for i in [0,1]: + if type(fractionVisible[i]) is bool: + fractionVisible[i] = 1.0 + + childRange = None + + order = [0,1] + if self.state['autoVisibleOnly'][0] is True: + order = [1,0] + + args = {} + for ax in order: + if self.state['autoRange'][ax] is False: + continue + if self.state['autoVisibleOnly'][ax]: + oRange = [None, None] + oRange[ax] = targetRect[1-ax] + childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange) - #targetRect[1][0] = childRect.top() - #targetRect[1][1] = childRect.bottom() - #args['yRange'] = targetRect[1] - if len(args) == 0: - return - args['padding'] = 0 - args['disableAutoRange'] = False - #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) - self.setRange(**args) + else: + if childRange is None: + childRange = self.childrenBounds(frac=fractionVisible) + + ## Make corrections to range + xr = childRange[ax] + if xr is not None: + if self.state['autoPan'][ax]: + x = sum(xr) * 0.5 + #x = childRect.center().x() + w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. + #childRect.setLeft(x-w2) + #childRect.setRight(x+w2) + childRange[ax] = [x-w2, x+w2] + else: + #wp = childRect.width() * 0.02 + wp = (xr[1] - xr[0]) * 0.02 + #childRect = childRect.adjusted(-wp, 0, wp, 0) + childRange[ax][0] -= wp + childRange[ax][1] += wp + #targetRect[ax][0] = childRect.left() + #targetRect[ax][1] = childRect.right() + targetRect[ax] = childRange[ax] + args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] + #else: + ### Make corrections to Y range + #if self.state['autoPan'][1]: + #y = childRect.center().y() + #h2 = (targetRect[1][1]-targetRect[1][0]) / 2. + #childRect.setTop(y-h2) + #childRect.setBottom(y+h2) + #else: + #hp = childRect.height() * 0.02 + #childRect = childRect.adjusted(0, -hp, 0, hp) + + #targetRect[1][0] = childRect.top() + #targetRect[1][1] = childRect.bottom() + #args['yRange'] = targetRect[1] + if len(args) == 0: + return + args['padding'] = 0 + args['disableAutoRange'] = False + #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) + self.setRange(**args) + finally: + self._updatingRange = False def setXLink(self, view): """Link this view's X axis to another view. (see LinkView)""" From eab1d7559259bc23ff021611be1f989a6879bb5a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 31 Oct 2012 02:01:55 -0400 Subject: [PATCH 4/5] ROI updates: - ROI.movePoint now expects parent coordinates by default - Added ROI.getHandles() - Renamed MultiLineROI to MultiRectROI - Reorganized MultiRectROI, added addSegment and removeSegment methods (thanks Martin!) --- examples/ROIExamples.py | 16 ++- graphicsItems/ROI.py | 239 +++++++++++++++++++--------------------- 2 files changed, 130 insertions(+), 125 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 044e0141..0a436319 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -36,12 +36,16 @@ img1a = pg.ImageItem(arr) v1a.addItem(img1a) img1b = pg.ImageItem() v1b.addItem(img1b) +v1a.disableAutoRange('xy') +v1b.disableAutoRange('xy') +v1a.autoRange() +v1b.autoRange() rois = [] rois.append(pg.RectROI([20, 20], [20, 20], pen=(0,9))) rois[-1].addRotateHandle([1,0], [0.5, 0.5]) rois.append(pg.LineROI([0, 60], [20, 80], width=5, pen=(1,9))) -rois.append(pg.MultiLineROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) +rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9))) rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9))) #rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9))) @@ -70,6 +74,10 @@ r2a = pg.PolyLineROI([[0,0], [10,10], [10,30], [30,10]], closed=True) v2a.addItem(r2a) r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False) v2a.addItem(r2b) +v2a.disableAutoRange('xy') +#v2b.disableAutoRange('xy') +v2a.autoRange() +#v2b.autoRange() text = """Building custom ROI types
ROIs can be built with a variety of different handle types
@@ -107,6 +115,9 @@ r3b.addRotateHandle([0, 1], [1, 0]) r3b.addScaleRotateHandle([0, 0.5], [0.5, 0.5]) r3b.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) +v3.disableAutoRange('xy') +v3.autoRange() + text = """Transforming objects with ROI""" w4 = w.addLayout(row=1, col=1) @@ -121,6 +132,9 @@ img4 = pg.ImageItem(arr) v4.addItem(r4) img4.setParentItem(r4) +v4.disableAutoRange('xy') +v4.autoRange() + diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 78ac1ad1..e3f094ff 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -28,7 +28,7 @@ from .UIGraphicsItem import UIGraphicsItem __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', ] @@ -370,7 +370,9 @@ class ROI(GraphicsObject): else: return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) - + def getHandles(self): + return [h['item'] for h in self.handles] + def mapSceneToParent(self, pt): return self.mapToParent(self.mapFromScene(pt)) @@ -538,19 +540,27 @@ class ROI(GraphicsObject): return True - def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): + def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'): ## called by Handles when they are moved. ## pos is the new position of the handle in scene coords, as requested by the handle. newState = self.stateCopy() index = self.indexOfHandle(handle) h = self.handles[index] - p0 = self.mapToScene(h['pos'] * self.state['size']) + p0 = self.mapToParent(h['pos'] * self.state['size']) p1 = Point(pos) + if coords == 'parent': + pass + elif coords == 'scene': + p1 = self.mapSceneToParent(p1) + else: + raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") + + ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. - p0 = self.mapSceneToParent(p0) - p1 = self.mapSceneToParent(p1) + #p0 = self.mapSceneToParent(p0) + #p1 = self.mapSceneToParent(p1) ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) if 'center' in h: @@ -566,8 +576,8 @@ class ROI(GraphicsObject): self.translate(p1-p0, snap=snap, update=False) elif h['type'] == 'f': - newPos = self.mapFromScene(pos) - h['item'].setPos(self.mapFromScene(pos)) + newPos = self.mapFromParent(p1) + h['item'].setPos(newPos) h['pos'] = newPos self.freeHandleMoved = True #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() @@ -1212,7 +1222,7 @@ class Handle(UIGraphicsItem): #print "point moved; inform %d ROIs" % len(self.roi) # A handle can be used by multiple ROIs; tell each to update its handle position for r in self.rois: - r.movePoint(self, pos, modifiers, finish=finish) + r.movePoint(self, pos, modifiers, finish=finish, coords='scene') def buildPath(self): size = self.radius @@ -1264,9 +1274,9 @@ class Handle(UIGraphicsItem): if self._shape is None: s = self.generateShape() if s is None: - return self.shape + return self.path self._shape = s - self.prepareGeometryChange() + self.prepareGeometryChange() ## beware--this can cause the view to adjust, which would immediately invalidate the shape. return self._shape def boundingRect(self): @@ -1357,9 +1367,16 @@ class LineROI(ROI): self.addScaleRotateHandle([1, 0.5], [0, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5]) + -class MultiLineROI(QtGui.QGraphicsObject): +class MultiRectROI(QtGui.QGraphicsObject): + """ + Chain of rectangular ROIs connected by handles. + This is generally used to mark a curved path through + an image similarly to PolyLineROI. It differs in that each segment + of the chain is rectangular instead of linear and thus has width. + """ sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) sigRegionChanged = QtCore.Signal(object) @@ -1368,27 +1385,17 @@ class MultiLineROI(QtGui.QGraphicsObject): QtGui.QGraphicsObject.__init__(self) self.pen = pen self.roiArgs = args + self.lines = [] if len(points) < 2: raise Exception("Must start with at least 2 points") - self.lines = [] - self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args)) - self.lines[-1].addScaleHandle([0.5, 1], [0.5, 0.5]) - h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) - h.movePoint(points[0]) - h.movePoint(points[0]) - for i in range(1, len(points)): - h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) - if i < len(points)-1: - self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args)) - self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=h) - h.movePoint(points[i]) - h.movePoint(points[i]) - - for l in self.lines: - l.translatable = False - l.sigRegionChanged.connect(self.roiChangedEvent) - l.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) - l.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) + + ## create first segment + self.addSegment(points[1], connectTo=points[0], scaleHandle=True) + + ## create remaining segments + for p in points[2:]: + self.addSegment(p) + def paint(self, *args): pass @@ -1411,7 +1418,13 @@ class MultiLineROI(QtGui.QGraphicsObject): def roiChangeFinishedEvent(self): self.sigRegionChangeFinished.emit(self) - + def getHandlePositions(self): + """Return the positions of all handles in local coordinates.""" + pos = [self.mapFromScene(self.lines[0].getHandles()[0].scenePos())] + for l in self.lines: + pos.append(self.mapFromScene(l.getHandles()[1].scenePos())) + return pos + def getArrayRegion(self, arr, img=None, axes=(0,1)): rgns = [] for l in self.lines: @@ -1432,6 +1445,59 @@ class MultiLineROI(QtGui.QGraphicsObject): return np.concatenate(rgns, axis=axes[0]) + def addSegment(self, pos=(0,0), scaleHandle=False, connectTo=None): + """ + Add a new segment to the ROI connecting from the previous endpoint to *pos*. + (pos is specified in the parent coordinate system of the MultiRectROI) + """ + + ## by default, connect to the previous endpoint + if connectTo is None: + connectTo = self.lines[-1].getHandles()[1] + + ## create new ROI + newRoi = ROI((0,0), [1, 5], parent=self, pen=self.pen, **self.roiArgs) + self.lines.append(newRoi) + + ## Add first SR handle + if isinstance(connectTo, Handle): + self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=connectTo) + newRoi.movePoint(connectTo, connectTo.scenePos(), coords='scene') + else: + h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) + newRoi.movePoint(h, connectTo, coords='scene') + + ## add second SR handle + h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) + newRoi.movePoint(h, pos) + + ## optionally add scale handle (this MUST come after the two SR handles) + if scaleHandle: + newRoi.addScaleHandle([0.5, 1], [0.5, 0.5]) + + newRoi.translatable = False + newRoi.sigRegionChanged.connect(self.roiChangedEvent) + newRoi.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) + newRoi.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) + self.sigRegionChanged.emit(self) + + + def removeSegment(self, index=-1): + """Remove a segment from the ROI.""" + roi = self.lines[index] + self.lines.pop(index) + self.scene().removeItem(roi) + roi.sigRegionChanged.disconnect(self.roiChangedEvent) + roi.sigRegionChangeStarted.disconnect(self.roiChangeStartedEvent) + roi.sigRegionChangeFinished.disconnect(self.roiChangeFinishedEvent) + + self.sigRegionChanged.emit(self) + + +class MultiLineROI(MultiRectROI): + def __init__(self, *args, **kwds): + MultiRectROI.__init__(self, *args, **kwds) + print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") class EllipseROI(ROI): def __init__(self, pos, size, **args): @@ -1475,6 +1541,8 @@ class CircleROI(EllipseROI): self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) class PolygonROI(ROI): + ## deprecated. Use PloyLineROI instead. + def __init__(self, positions, pos=None, **args): if pos is None: pos = [0,0] @@ -1483,16 +1551,17 @@ class PolygonROI(ROI): for p in positions: self.addFreeHandle(p) self.setZValue(1000) + print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.") def listPoints(self): return [p['item'].pos() for p in self.handles] - def movePoint(self, *args, **kargs): - ROI.movePoint(self, *args, **kargs) - self.prepareGeometryChange() - for h in self.handles: - h['pos'] = h['item'].pos() + #def movePoint(self, *args, **kargs): + #ROI.movePoint(self, *args, **kargs) + #self.prepareGeometryChange() + #for h in self.handles: + #h['pos'] = h['item'].pos() def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) @@ -1687,103 +1756,33 @@ class LineSegmentROI(ROI): ROI.__init__(self, pos, [1,1], **args) #ROI.__init__(self, positions[0]) if len(positions) > 2: - raise Exception("LineSegmentROI can only be defined by 2 positions. This is an API change.") + raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") for i, p in enumerate(positions): self.addFreeHandle(p, item=handles[i]) - #self.setZValue(1000) - #self.parentROI = None - #self.hasParentROI = False - #self.setAcceptsHandles(acceptsHandles) - - #def setParentROI(self, parent): - #self.parentROI = parent - #if parent != None: - #self.hasParentROI = True - #else: - #self.hasParentROI = False - - #def setAcceptsHandles(self, b): - #if b: - #self.setAcceptedMouseButtons(QtCore.Qt.LeftButton) - #else: - #self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - - #def close(self): - ##for h in self.handles: - ##if len(h['item'].roi) == 1: - ##h['item'].scene().removeItem(h['item']) - ##elif h['item'].parentItem() == self: - ##h['item'].setParentItem(self.parentItem()) - - #self.scene().removeItem(self) - - #def handleRemoved(self, handle): - #self.parentROI.handleRemoved(self, handle) - - #def hoverEvent(self, ev): - #if (self.translatable or self.acceptsHandles) and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): - ##print " setHover: True" - #self.setMouseHover(True) - #self.sigHoverEvent.emit(self) - #else: - ##print " setHover: False" - #self.setMouseHover(False) - - #def mouseClickEvent(self, ev): - #ROI.mouseClickEvent(self, ev) ## only checks for Right-clicks (for now anyway) - #if ev.button() == QtCore.Qt.LeftButton: - #if self.acceptsHandles: - #ev.accept() - #self.newHandleRequested(ev.pos()) ## ev.pos is the position in this item's coordinates - #else: - #ev.ignore() - - #def newHandleRequested(self, evPos): - #print "newHandleRequested" - - #if evPos - self.handles[0].pos() == Point(0.,0.) or evPos-handles[1].pos() == Point(0.,0.): - # return - #self.parentROI.newHandleRequested(self, self.mapToParent(evPos)) ## so now evPos should be passed in in the parents coordinate system def listPoints(self): return [p['item'].pos() for p in self.handles] - #def movePoint(self, *args, **kargs): - #ROI.movePoint(self, *args, **kargs) - #self.prepareGeometryChange() - #for h in self.handles: - #h['pos'] = h['item'].pos() - def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) h1 = self.handles[0]['item'].pos() h2 = self.handles[1]['item'].pos() p.drawLine(h1, h2) - #p.setPen(fn.mkPen('w')) - #p.drawPath(self.shape()) - - #for i in range(len(self.handles)-1): - #h1 = self.handles[i]['item'].pos() - #h2 = self.handles[i+1]['item'].pos() - #p.drawLine(h1, h2) def boundingRect(self): return self.shape().boundingRect() - #r = QtCore.QRectF() - #for h in self.handles: - #r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs - #return r def shape(self): p = QtGui.QPainterPath() - #pw, ph = self.pixelSize() - #pHyp = 4 * (pw**2 + ph**2)**0.5 h1 = self.handles[0]['item'].pos() h2 = self.handles[1]['item'].pos() + dh = h2-h1 + if dh.length() == 0: + return p pxv = self.pixelVectors(h2-h1)[1] if pxv is None: @@ -1799,14 +1798,6 @@ class LineSegmentROI(ROI): return p - #def stateCopy(self): - #sc = {} - #sc['pos'] = Point(self.state['pos']) - #sc['size'] = Point(self.state['size']) - #sc['angle'] = self.state['angle'] - ##sc['handles'] = self.handles - #return sc - def getArrayRegion(self, data, img, axes=(0,1)): """ Use the position of this ROI relative to an imageItem to pull a slice from an array. @@ -1849,11 +1840,11 @@ class SpiralROI(ROI): return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) #return self.bounds - def movePoint(self, *args, **kargs): - ROI.movePoint(self, *args, **kargs) - self.prepareGeometryChange() - for h in self.handles: - h['pos'] = h['item'].pos()/self.state['size'][0] + #def movePoint(self, *args, **kargs): + #ROI.movePoint(self, *args, **kargs) + #self.prepareGeometryChange() + #for h in self.handles: + #h['pos'] = h['item'].pos()/self.state['size'][0] def stateChanged(self): ROI.stateChanged(self) From a5a40be8bb877f40a90b9b515f75a7a1e3288f4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 31 Oct 2012 02:07:19 -0400 Subject: [PATCH 5/5] Bugfix: GLScatterPlotItem would not display when using non-array size and pxMode=True --- opengl/items/GLScatterPlotItem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opengl/items/GLScatterPlotItem.py b/opengl/items/GLScatterPlotItem.py index 1134ce51..9ff22c37 100644 --- a/opengl/items/GLScatterPlotItem.py +++ b/opengl/items/GLScatterPlotItem.py @@ -139,7 +139,8 @@ class GLScatterPlotItem(GLGraphicsItem): glNormalPointerf(norm) else: - glPointSize(self.size) + glNormal3f(self.size,0,0) + #glPointSize(self.size) glDrawArrays(GL_POINTS, 0, len(self.pos)) finally: glDisableClientState(GL_NORMAL_ARRAY) @@ -174,4 +175,4 @@ class GLScatterPlotItem(GLGraphicsItem): - \ No newline at end of file +