Bugfixes:

- Image exporter detects GraphicsView background color
- Corrected exporter filename handling for SVG export
- ViewBox no longer attempts to deregister itself while python is exiting
- Speedup for conversion using np.array(MetaArray_instance)
- GLGraphicsItem updates immediately when its GL options have changed
- Corrected some GL shader program bugs for nVidia drivers
- Fixed coordinate mapping bug in functions.transformCoordinates
- Fixed PySide import error
This commit is contained in:
Luke Campagnola 2012-12-04 21:02:05 -05:00
parent 9cb199d971
commit 80148920c9
17 changed files with 150 additions and 33 deletions

2
Qt.py
View File

@ -14,7 +14,7 @@ else:
USE_PYSIDE = False
except ImportError:
try:
import Pyside
import PySide
USE_PYSIDE = True
except ImportError:
raise Exception("PyQtGraph requires either PyQt4 or PySide; neither package could be imported.")

View File

@ -173,11 +173,14 @@ from .SignalProxy import *
from .ptime import time
## Workaround for Qt exit crash:
## ALL QGraphicsItems must have a scene before they are deleted.
## This is potentially very expensive, but preferred over crashing.
import atexit
def cleanup():
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
## Workaround for Qt exit crash:
## ALL QGraphicsItems must have a scene before they are deleted.
## This is potentially very expensive, but preferred over crashing.
## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer..
if QtGui.QApplication.instance() is None:
return
import gc

View File

@ -0,0 +1,14 @@
import os
dirs = [
('graphicsItems', 'graphicsItems'),
('3dgraphics', 'opengl/items'),
('widgets', 'widgets'),
]
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
for a, b in dirs:
rst = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, 'documentation', 'source', a))]
py = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, b))]
print a
for x in set(py) - set(rst):
print " ", x

View File

@ -1,7 +1,7 @@
from pyqtgraph.widgets.FileDialog import FileDialog
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import os
import os, re
LastExportDirectory = None
@ -55,10 +55,13 @@ class Exporter(object):
global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0]
ext = os.path.splitext(fileName)[1].lower()
selectedExt = str(self.fileDialog.selectedNameFilter()).lstrip('*').lower()
if ext != selectedExt:
fileName = fileName + selectedExt
## If file name does not match selected extension, append it now
ext = os.path.splitext(fileName)[1].lower().lstrip('.')
selectedExt = re.search(r'\*\.(\w+)\b', str(self.fileDialog.selectedNameFilter()))
if selectedExt is not None:
selectedExt = selectedExt.groups()[0].lower()
if ext != selectedExt:
fileName = fileName + selectedExt
self.export(fileName=fileName, **self.fileDialog.opts)

View File

@ -11,12 +11,16 @@ class ImageExporter(Exporter):
def __init__(self, item):
Exporter.__init__(self, item)
tr = self.getTargetRect()
if isinstance(item, QtGui.QGraphicsItem):
scene = item.scene()
else:
scene = item
bg = scene.views()[0].backgroundBrush().color()
self.params = Parameter(name='params', type='group', children=[
{'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
{'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
{'name': 'antialias', 'type': 'bool', 'value': True},
{'name': 'background', 'type': 'color', 'value': (0,0,0,255)},
{'name': 'background', 'type': 'color', 'value': bg},
])
self.params.param('width').sigValueChanged.connect(self.widthChanged)
self.params.param('height').sigValueChanged.connect(self.heightChanged)

View File

@ -290,3 +290,47 @@ def suggestDType(x):
#return '<U%d' % len(x)
else:
return object
def removePeriodic(data, f0=60.0, dt=None, harmonics=10, samples=4):
if (hasattr(data, 'implements') and data.implements('MetaArray')):
data1 = data.asarray()
if dt is None:
times = data.xvals('Time')
dt = times[1]-times[0]
else:
data1 = data
if dt is None:
raise Exception('Must specify dt for this data')
ft = np.fft.fft(data1)
## determine frequencies in fft data
df = 1.0 / (len(data1) * dt)
freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft))
## flatten spikes at f0 and harmonics
for i in xrange(1, harmonics + 2):
f = f0 * i # target frequency
## determine index range to check for this frequency
ind1 = int(np.floor(f / df))
ind2 = int(np.ceil(f / df)) + (samples-1)
if ind1 > len(ft)/2.:
break
mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5
for j in range(ind1, ind2+1):
phase = np.angle(ft[j]) ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts.
re = mag * np.cos(phase)
im = mag * np.sin(phase)
ft[j] = re + im*1j
ft[len(ft)-j] = re - im*1j
data2 = np.fft.ifft(ft).real
if (hasattr(data, 'implements') and data.implements('MetaArray')):
return metaarray.MetaArray(data2, info=data.infoCopy())
else:
return data2

View File

@ -491,6 +491,9 @@ def transformToArray(tr):
## map coordinates through transform
mapped = np.dot(m, coords)
"""
if isinstance(tr, np.ndarray):
return tr
#return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
## The order of elements given by the method names m11..m33 is misleading--
## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in
@ -510,8 +513,11 @@ def transformCoordinates(tr, coords):
The mapping will _ignore_ any perspective transformations.
"""
nd = coords.shape[0]
m = transformToArray(tr)
m = m[:m.shape[0]-1] # remove perspective
if not isinstance(tr, np.ndarray):
m = transformToArray(tr)
m = m[:m.shape[0]-1] # remove perspective
else:
m = tr
## If coords are 3D and tr is 2D, assume no change for Z axis
if m.shape == (2,3) and nd == 3:
@ -537,7 +543,7 @@ def transformCoordinates(tr, coords):
m = m[:, :-1]
## map coordinates and return
mapped = (m*coords).sum(axis=0) ## apply scale/rotate
mapped = (m*coords).sum(axis=1) ## apply scale/rotate
mapped += translate
return mapped

View File

@ -281,6 +281,11 @@ class ImageItem(GraphicsObject):
p.drawRect(self.boundingRect())
prof.finish()
def save(self, fileName, *args):
"""Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
if self.qimage is None:
self.render()
self.qimage.save(fileName, *args)
def getHistogram(self, bins=500, step=3):
"""Returns x and y arrays containing the histogram values for the current image.

View File

@ -1222,4 +1222,15 @@ class ViewBox(GraphicsWidget):
ViewBox.NamedViews.pop(name, None)
ViewBox.updateAllViewLists()
@staticmethod
def quit():
## called when the application is about to exit.
## this disables all callbacks, which might otherwise generate errors if invoked during exit.
for k in ViewBox.AllViews:
try:
k.destroyed.disconnect()
except RuntimeError: ## signal is already disconnected.
pass
from .ViewBoxMenu import ViewBoxMenu

View File

@ -343,6 +343,10 @@ class MetaArray(object):
else:
return np.array(self._data)
def __array__(self):
## supports np.array(metaarray_instance)
return self.asarray()
def view(self, typ):
## deprecated; kept for backward compatibility
if typ is np.ndarray:

View File

@ -86,6 +86,7 @@ class GLGraphicsItem(QtCore.QObject):
if isinstance(opts, basestring):
opts = GLOptions[opts]
self.__glOpts = opts.copy()
self.update()
def updateGLOptions(self, opts):
"""

View File

@ -444,6 +444,10 @@ class MeshData(object):
import pickle
state = pickle.loads(state)
for k in state:
if isinstance(state[k], list):
if isinstance(state[k][0], QtGui.QVector3D):
state[k] = [[v.x(), v.y(), v.z()] for v in state[k]]
state[k] = np.array(state[k])
setattr(self, k, state[k])

View File

@ -22,3 +22,9 @@ from pyqtgraph import importAll
#globals()[k] = getattr(mod, k)
importAll('items', globals(), locals())
\
from MeshData import MeshData
## for backward compatibility:
MeshData.MeshData = MeshData
import shaders

View File

@ -55,12 +55,18 @@ class GLMeshItem(GLGraphicsItem):
self.faces = None
def setShader(self, shader):
"""Set the shader used when rendering faces in the mesh. (see the GL shaders example)"""
self.opts['shader'] = shader
self.update()
def shader(self):
return shaders.getShaderProgram(self.opts['shader'])
def setColor(self, c):
"""Set the default color to use when no vertex or face colors are specified."""
self.opts['color'] = c
self.update()
def setMeshData(self, **kwds):
"""
Set mesh data for this item. This can be invoked two ways:

View File

@ -49,9 +49,9 @@ def initShaders():
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;
color.x = (normal.x + 1.0) * 0.5;
color.y = (normal.y + 1.0) * 0.5;
color.z = (normal.z + 1.0) * 0.5;
gl_FragColor = color;
}
""")
@ -73,9 +73,9 @@ def initShaders():
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;
color.x = (normal.x + 1.0) * 0.5;
color.y = (normal.y + 1.0) * 0.5;
color.z = (normal.z + 1.0) * 0.5;
gl_FragColor = color;
}
""")
@ -97,7 +97,7 @@ def initShaders():
FragmentShader("""
varying vec3 normal;
void main() {
float p = dot(normal, normalize(vec3(1, -1, -1)));
float p = dot(normal, normalize(vec3(1.0, -1.0, -1.0)));
p = p < 0. ? 0. : p * 0.8;
vec4 color = gl_Color;
color.x = color.x * (0.2 + p);
@ -151,27 +151,26 @@ def initShaders():
}
"""),
FragmentShader("""
#version 140 // required for uniform blocks
uniform float colorMap[9];
varying vec4 pos;
out vec4 gl_FragColor;
in vec4 gl_Color;
//out vec4 gl_FragColor; // only needed for later glsl versions
//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.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.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.z = color.z < 0. ? 0. : (color.z > 1. ? 1. : color.z);
color.w = 1.0;
gl_FragColor = color;
@ -234,9 +233,9 @@ class Shader(object):
msg = msg.strip()
if msg == '':
continue
m = re.match(r'\d+\((\d+)\)', msg)
m = re.match(r'(\d+\:)?\d+\((\d+)\)', msg)
if m is not None:
line = int(m.groups()[0])
line = int(m.groups()[1])
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)

View File

@ -11,9 +11,9 @@ setup(name='pyqtgraph',
description='Scientific Graphics and GUI Library for Python',
long_description="PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display.",
license='MIT',
url='http://www.pyqtgraph.org',
author='Luke Campagnola',
author_email='luke.campagnola@gmail.com',
url='',
packages=all_packages,
package_dir = {'pyqtgraph': '.'},
package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']},

View File

@ -108,10 +108,13 @@ class SpinBox(QtGui.QAbstractSpinBox):
'suffix': '',
'siPrefix': False, ## Set to True to display numbers with SI prefix (ie, 100pA instead of 1e-10A)
'delay': 0.3, ## delay sending wheel update signals for 300ms
'delayUntilEditFinished': True, ## do not send signals until text editing has finished
## for compatibility with QDoubleSpinBox and QSpinBox
'decimals': 2
'decimals': 2,
}
self.decOpts = ['step', 'minStep']
@ -125,7 +128,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
self.editingFinished.connect(self.editingFinishedEvent)
self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange)
self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay'])
def event(self, ev):
ret = QtGui.QAbstractSpinBox.event(self, ev)
@ -140,6 +143,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
allowed in :func:`__init__ <pyqtgraph.SpinBox.__init__>`.
"""
#print opts
for k in opts:
if k == 'bounds':
#print opts[k]
@ -182,7 +186,10 @@ class SpinBox(QtGui.QAbstractSpinBox):
if ms < 1:
ms = 1
self.opts['minStep'] = ms
if 'delay' in opts:
self.proxy.setDelay(opts['delay'])
self.updateText()