Squashed commit of the following:

commit ca3fbe2ff9
Author: Luke Campagnola <luke.campagnola@gmail.com>
Date:   Thu Aug 7 08:41:30 2014 -0400

    Merged numerous updates from acq4:
    * Added HDF5 exporter
    * CSV exporter gets (x,y,y,y) export mode
    * Updates to SVG, Matplotlib exporter
    * Console can filter exceptions by string
    * Added tick context menu to GradientEditorItem
    * Added export feature to imageview
    * Parameter trees:
        - Option to save only user-editable values
        - Option to set visible title of parameters separately from name
        - Added experimental ParameterSystem for handling large systems of
            interdependent parameters
        - Auto-select editable portion of spinbox when editing
    * Added Vector.__abs__
    * Added replacement garbage collector for avoiding crashes on multithreaded Qt
    * Fixed "illegal instruction" caused by closing file handle 7 on OSX
    * configfile now reloads QtCore objects, Point, ColorMap, numpy arrays
    * Avoid triggering recursion issues in exception handler
    * Various bugfies and performance enhancements
This commit is contained in:
Luke Campagnola 2014-08-07 09:03:26 -04:00
parent 55a07b0bec
commit 753ac9b4c4
51 changed files with 1913 additions and 541 deletions

View File

@ -81,5 +81,7 @@ class Vector(QtGui.QVector3D):
# ang *= -1.
return ang * 180. / np.pi
def __abs__(self):
return Vector(abs(self.x()), abs(self.y()), abs(self.z()))

View File

@ -325,8 +325,13 @@ def exit():
atexit._run_exitfuncs()
## close file handles
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
if sys.platform == 'darwin':
for fd in xrange(3, 4096):
if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac.
os.close(fd)
else:
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
os._exit(0)

View File

@ -67,8 +67,8 @@ class Canvas(QtGui.QWidget):
self.ui.itemList.sigItemMoved.connect(self.treeItemMoved)
self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected)
self.ui.autoRangeBtn.clicked.connect(self.autoRange)
self.ui.storeSvgBtn.clicked.connect(self.storeSvg)
self.ui.storePngBtn.clicked.connect(self.storePng)
#self.ui.storeSvgBtn.clicked.connect(self.storeSvg)
#self.ui.storePngBtn.clicked.connect(self.storePng)
self.ui.redirectCheck.toggled.connect(self.updateRedirect)
self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect)
self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged)
@ -94,11 +94,13 @@ class Canvas(QtGui.QWidget):
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
def storeSvg(self):
self.ui.view.writeSvg()
#def storeSvg(self):
#from pyqtgraph.GraphicsScene.exportDialog import ExportDialog
#ex = ExportDialog(self.ui.view)
#ex.show()
def storePng(self):
self.ui.view.writeImage()
#def storePng(self):
#self.ui.view.writeImage()
def splitterMoved(self):
self.resizeEvent()
@ -571,7 +573,9 @@ class Canvas(QtGui.QWidget):
self.menu.popup(ev.globalPos())
def removeClicked(self):
self.removeItem(self.menuItem)
#self.removeItem(self.menuItem)
for item in self.selectedItems():
self.removeItem(item)
self.menuItem = None
import gc
gc.collect()

View File

@ -28,21 +28,7 @@
<widget class="GraphicsView" name="view"/>
<widget class="QWidget" name="layoutWidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QPushButton" name="storeSvgBtn">
<property name="text">
<string>Store SVG</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="storePngBtn">
<property name="text">
<string>Store PNG</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="autoRangeBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@ -55,7 +41,7 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
@ -75,7 +61,7 @@
</item>
</layout>
</item>
<item row="7" column="0" colspan="2">
<item row="6" column="0" colspan="2">
<widget class="TreeWidget" name="itemList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -93,28 +79,28 @@
</column>
</widget>
</item>
<item row="11" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<layout class="QGridLayout" name="ctrlLayout">
<property name="spacing">
<number>0</number>
</property>
</layout>
</item>
<item row="8" column="0">
<item row="7" column="0">
<widget class="QPushButton" name="resetTransformsBtn">
<property name="text">
<string>Reset Transforms</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QPushButton" name="mirrorSelectionBtn">
<property name="text">
<string>Mirror Selection</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QPushButton" name="reflectSelectionBtn">
<property name="text">
<string>MirrorXY</string>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui'
# Form implementation generated from reading ui file 'acq4/pyqtgraph/canvas/CanvasTemplate.ui'
#
# Created: Mon Dec 23 10:10:52 2013
# by: PyQt4 UI code generator 4.10
# Created: Thu Jan 2 11:13:07 2014
# by: PyQt4 UI code generator 4.9
#
# WARNING! All changes made in this file will be lost!
@ -12,16 +12,7 @@ from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
_fromUtf8 = lambda s: s
class Ui_Form(object):
def setupUi(self, Form):
@ -41,12 +32,6 @@ class Ui_Form(object):
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget)
self.gridLayout_2.setMargin(0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget)
self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn"))
self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1)
self.storePngBtn = QtGui.QPushButton(self.layoutWidget)
self.storePngBtn.setObjectName(_fromUtf8("storePngBtn"))
self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1)
self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@ -54,7 +39,7 @@ class Ui_Form(object):
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
self.autoRangeBtn.setSizePolicy(sizePolicy)
self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn"))
self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2)
self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
@ -64,7 +49,7 @@ class Ui_Form(object):
self.redirectCombo = CanvasCombo(self.layoutWidget)
self.redirectCombo.setObjectName(_fromUtf8("redirectCombo"))
self.horizontalLayout.addWidget(self.redirectCombo)
self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2)
self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2)
self.itemList = TreeWidget(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
@ -74,35 +59,33 @@ class Ui_Form(object):
self.itemList.setHeaderHidden(True)
self.itemList.setObjectName(_fromUtf8("itemList"))
self.itemList.headerItem().setText(0, _fromUtf8("1"))
self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2)
self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2)
self.ctrlLayout = QtGui.QGridLayout()
self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout"))
self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2)
self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2)
self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget)
self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn"))
self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1)
self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1)
self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget)
self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn"))
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1)
self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget)
self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn"))
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
self.storeSvgBtn.setText(_translate("Form", "Store SVG", None))
self.storePngBtn.setText(_translate("Form", "Store PNG", None))
self.autoRangeBtn.setText(_translate("Form", "Auto Range", None))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None))
self.redirectCheck.setText(_translate("Form", "Redirect", None))
self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None))
self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None))
self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))
self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8))
self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8))
self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8))
from ..widgets.TreeWidget import TreeWidget
from CanvasManager import CanvasCombo

View File

@ -244,4 +244,7 @@ class ColorMap(object):
else:
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]]))
def __repr__(self):
pos = repr(self.pos).replace('\n', '')
color = repr(self.color).replace('\n', '')
return "ColorMap(%s, %s)" % (pos, color)

View File

@ -14,6 +14,10 @@ from .pgcollections import OrderedDict
GLOBAL_PATH = None # so not thread safe.
from . import units
from .python2_3 import asUnicode
from .Qt import QtCore
from .Point import Point
from .colormap import ColorMap
import numpy
class ParseError(Exception):
def __init__(self, message, lineNum, line, fileName=None):
@ -46,7 +50,7 @@ def readConfigFile(fname):
fname2 = os.path.join(GLOBAL_PATH, fname)
if os.path.exists(fname2):
fname = fname2
GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))
try:
@ -135,6 +139,17 @@ def parseString(lines, start=0):
local = units.allUnits.copy()
local['OrderedDict'] = OrderedDict
local['readConfigFile'] = readConfigFile
local['Point'] = Point
local['QtCore'] = QtCore
local['ColorMap'] = ColorMap
# Needed for reconstructing numpy arrays
local['array'] = numpy.array
for dtype in ['int8', 'uint8',
'int16', 'uint16', 'float16',
'int32', 'uint32', 'float32',
'int64', 'uint64', 'float64']:
local[dtype] = getattr(numpy, dtype)
if len(k) < 1:
raise ParseError('Missing name preceding colon', ln+1, l)
if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it.

View File

@ -341,6 +341,17 @@ class ConsoleWidget(QtGui.QWidget):
filename = tb.tb_frame.f_code.co_filename
function = tb.tb_frame.f_code.co_name
filterStr = str(self.ui.filterText.text())
if filterStr != '':
if isinstance(exc, Exception):
msg = exc.message
elif isinstance(exc, basestring):
msg = exc
else:
msg = repr(exc)
match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg))
return match is not None
## Go through a list of common exception points we like to ignore:
if excType is GeneratorExit or excType is StopIteration:
return False

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>710</width>
<width>694</width>
<height>497</height>
</rect>
</property>
@ -89,6 +89,16 @@
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="6">
<widget class="QPushButton" name="clearExceptionBtn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Clear Exception</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="catchAllExceptionsBtn">
<property name="text">
@ -109,7 +119,7 @@
</property>
</widget>
</item>
<item row="0" column="2">
<item row="0" column="4">
<widget class="QCheckBox" name="onlyUncaughtCheck">
<property name="text">
<string>Only Uncaught Exceptions</string>
@ -119,14 +129,14 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="5">
<item row="2" column="0" colspan="7">
<widget class="QListWidget" name="exceptionStackList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0" colspan="5">
<item row="3" column="0" colspan="7">
<widget class="QCheckBox" name="runSelectedFrameCheck">
<property name="text">
<string>Run commands in selected stack frame</string>
@ -136,24 +146,14 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="5">
<item row="1" column="0" colspan="7">
<widget class="QLabel" name="exceptionInfoLabel">
<property name="text">
<string>Exception Info</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="clearExceptionBtn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Clear Exception</string>
</property>
</widget>
</item>
<item row="0" column="3">
<item row="0" column="5">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -166,6 +166,16 @@
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label">
<property name="text">
<string>Filter (regex):</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="filterText"/>
</item>
</layout>
</widget>
</widget>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
# Form implementation generated from reading ui file 'template.ui'
#
# Created: Mon Dec 23 10:10:53 2013
# by: PyQt4 UI code generator 4.10
# Created: Fri May 02 18:55:28 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@ -26,7 +26,7 @@ except AttributeError:
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName(_fromUtf8("Form"))
Form.resize(710, 497)
Form.resize(694, 497)
self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setMargin(0)
self.gridLayout.setSpacing(0)
@ -71,6 +71,10 @@ class Ui_Form(object):
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn"))
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup)
self.catchAllExceptionsBtn.setCheckable(True)
self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn"))
@ -82,24 +86,26 @@ class Ui_Form(object):
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
self.onlyUncaughtCheck.setChecked(True)
self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck"))
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup)
self.exceptionStackList.setAlternatingRowColors(True)
self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList"))
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup)
self.runSelectedFrameCheck.setChecked(True)
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck"))
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn"))
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
self.label = QtGui.QLabel(self.exceptionGroup)
self.label.setObjectName(_fromUtf8("label"))
self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
self.filterText = QtGui.QLineEdit(self.exceptionGroup)
self.filterText.setObjectName(_fromUtf8("filterText"))
self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form)
@ -110,11 +116,12 @@ class Ui_Form(object):
self.historyBtn.setText(_translate("Form", "History..", None))
self.exceptionBtn.setText(_translate("Form", "Exceptions..", None))
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None))
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None))
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None))
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None))
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None))
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None))
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
self.label.setText(_translate("Form", "Filter (regex):", None))
from .CmdInput import CmdInput

View File

@ -32,6 +32,57 @@ def ftrace(func):
return rv
return w
class Tracer(object):
"""
Prints every function enter/exit. Useful for debugging crashes / lockups.
"""
def __init__(self):
self.count = 0
self.stack = []
def trace(self, frame, event, arg):
self.count += 1
# If it has been a long time since we saw the top of the stack,
# print a reminder
if self.count % 1000 == 0:
print("----- current stack: -----")
for line in self.stack:
print(line)
if event == 'call':
line = " " * len(self.stack) + ">> " + self.frameInfo(frame)
print(line)
self.stack.append(line)
elif event == 'return':
self.stack.pop()
line = " " * len(self.stack) + "<< " + self.frameInfo(frame)
print(line)
if len(self.stack) == 0:
self.count = 0
return self.trace
def stop(self):
sys.settrace(None)
def start(self):
sys.settrace(self.trace)
def frameInfo(self, fr):
filename = fr.f_code.co_filename
funcname = fr.f_code.co_name
lineno = fr.f_lineno
callfr = sys._getframe(3)
callline = "%s %d" % (callfr.f_code.co_name, callfr.f_lineno)
args, _, _, value_dict = inspect.getargvalues(fr)
if len(args) and args[0] == 'self':
instance = value_dict.get('self', None)
if instance is not None:
cls = getattr(instance, '__class__', None)
if cls is not None:
funcname = cls.__name__ + "." + funcname
return "%s: %s %s: %s" % (callline, filename, lineno, funcname)
def warnOnException(func):
"""Decorator which catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds):
@ -41,17 +92,22 @@ def warnOnException(func):
printExc('Ignored exception:')
return w
def getExc(indent=4, prefix='| '):
tb = traceback.format_exc()
lines = []
for l in tb.split('\n'):
lines.append(" "*indent + prefix + l)
return '\n'.join(lines)
def getExc(indent=4, prefix='| ', skip=1):
lines = (traceback.format_stack()[:-skip]
+ [" ---- exception caught ---->\n"]
+ traceback.format_tb(sys.exc_info()[2])
+ traceback.format_exception_only(*sys.exc_info()[:2]))
lines2 = []
for l in lines:
lines2.extend(l.strip('\n').split('\n'))
lines3 = [" "*indent + prefix + l for l in lines2]
return '\n'.join(lines3)
def printExc(msg='', indent=4, prefix='|'):
"""Print an error message followed by an indented exception backtrace
(This function is intended to be called within except: blocks)"""
exc = getExc(indent, prefix + ' ')
exc = getExc(indent, prefix + ' ', skip=2)
print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
print(" "*indent + prefix + '='*30 + '>>')
print(exc)
@ -407,6 +463,7 @@ class Profiler(object):
_depth = 0
_msgs = []
disable = False # set this flag to disable all or individual profilers at runtime
class DisabledProfiler(object):
def __init__(self, *args, **kwds):
@ -418,12 +475,11 @@ class Profiler(object):
def mark(self, msg=None):
pass
_disabledProfiler = DisabledProfiler()
def __new__(cls, msg=None, disabled='env', delayed=True):
"""Optionally create a new profiler based on caller's qualname.
"""
if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
if disabled is True or (disabled == 'env' and len(cls._profilers) == 0):
return cls._disabledProfiler
# determine the qualified name of the caller function
@ -431,11 +487,11 @@ class Profiler(object):
try:
caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
else: # we are in a method
qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_name
if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
if disabled == 'env' and func_qualname not in cls._profilers: # don't do anything
return cls._disabledProfiler
# create an actual profiling object
cls._depth += 1
@ -447,13 +503,12 @@ class Profiler(object):
obj._firstTime = obj._lastTime = ptime.time()
obj._newMsg("> Entering " + obj._name)
return obj
#else:
#def __new__(cls, delayed=True):
#return lambda msg=None: None
def __call__(self, msg=None):
"""Register or print a new message with timing information.
"""
if self.disable:
return
if msg is None:
msg = str(self._markCount)
self._markCount += 1
@ -479,7 +534,7 @@ class Profiler(object):
def finish(self, msg=None):
"""Add a final message; flush the message list if no parent profiler.
"""
if self._finished:
if self._finished or self.disable:
return
self._finished = True
if msg is not None:
@ -984,6 +1039,7 @@ def qObjectReport(verbose=False):
class PrintDetector(object):
"""Find code locations that print to stdout."""
def __init__(self):
self.stdout = sys.stdout
sys.stdout = self
@ -1002,6 +1058,45 @@ class PrintDetector(object):
self.stdout.flush()
def listQThreads():
"""Prints Thread IDs (Qt's, not OS's) for all QThreads."""
thr = findObj('[Tt]hread')
thr = [t for t in thr if isinstance(t, QtCore.QThread)]
import sip
for t in thr:
print("--> ", t)
print(" Qt ID: 0x%x" % sip.unwrapinstance(t))
def pretty(data, indent=''):
"""Format nested dict/list/tuple structures into a more human-readable string
This function is a bit better than pprint for displaying OrderedDicts.
"""
ret = ""
ind2 = indent + " "
if isinstance(data, dict):
ret = indent+"{\n"
for k, v in data.iteritems():
ret += ind2 + repr(k) + ": " + pretty(v, ind2).strip() + "\n"
ret += indent+"}\n"
elif isinstance(data, list) or isinstance(data, tuple):
s = repr(data)
if len(s) < 40:
ret += indent + s
else:
if isinstance(data, list):
d = '[]'
else:
d = '()'
ret = indent+d[0]+"\n"
for i, v in enumerate(data):
ret += ind2 + str(i) + ": " + pretty(v, ind2).strip() + "\n"
ret += indent+d[1]+"\n"
else:
ret += indent + repr(data)
return ret
class PeriodicTrace(object):
"""
Used to debug freezing by starting a new thread that reports on the

View File

@ -49,29 +49,45 @@ def setTracebackClearing(clear=True):
class ExceptionHandler(object):
def __call__(self, *args):
## call original exception handler first (prints exception)
global original_excepthook, callbacks, clear_tracebacks
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
ret = original_excepthook(*args)
## Start by extending recursion depth just a bit.
## If the error we are catching is due to recursion, we don't want to generate another one here.
recursionLimit = sys.getrecursionlimit()
try:
sys.setrecursionlimit(recursionLimit+100)
for cb in callbacks:
## call original exception handler first (prints exception)
global original_excepthook, callbacks, clear_tracebacks
try:
cb(*args)
except:
print(" --------------------------------------------------------------")
print(" Error occurred during exception callback %s" % str(cb))
print(" --------------------------------------------------------------")
traceback.print_exception(*sys.exc_info())
## Clear long-term storage of last traceback to prevent memory-hogging.
## (If an exception occurs while a lot of data is present on the stack,
## such as when loading large files, the data would ordinarily be kept
## until the next exception occurs. We would rather release this memory
## as soon as possible.)
if clear_tracebacks is True:
sys.last_traceback = None
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
except Exception:
sys.stderr.write("Warning: stdout is broken! Falling back to stderr.\n")
sys.stdout = sys.stderr
ret = original_excepthook(*args)
for cb in callbacks:
try:
cb(*args)
except Exception:
print(" --------------------------------------------------------------")
print(" Error occurred during exception callback %s" % str(cb))
print(" --------------------------------------------------------------")
traceback.print_exception(*sys.exc_info())
## Clear long-term storage of last traceback to prevent memory-hogging.
## (If an exception occurs while a lot of data is present on the stack,
## such as when loading large files, the data would ordinarily be kept
## until the next exception occurs. We would rather release this memory
## as soon as possible.)
if clear_tracebacks is True:
sys.last_traceback = None
finally:
sys.setrecursionlimit(recursionLimit)
def implements(self, interface=None):
## this just makes it easy for us to detect whether an ExceptionHook is already installed.
if interface is None:

View File

@ -14,6 +14,7 @@ class CSVExporter(Exporter):
self.params = Parameter(name='params', type='group', children=[
{'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']},
{'name': 'precision', 'type': 'int', 'value': 10, 'limits': [0, None]},
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']}
])
def parameters(self):
@ -31,15 +32,24 @@ class CSVExporter(Exporter):
fd = open(fileName, 'w')
data = []
header = []
for c in self.item.curves:
appendAllX = self.params['columnMode'] == '(x,y) per plot'
for i, c in enumerate(self.item.curves):
cd = c.getData()
if cd[0] is None:
continue
data.append(cd)
name = ''
if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None:
name = c.name().replace('"', '""') + '_'
header.extend(['"'+name+'x"', '"'+name+'y"'])
xName, yName = '"'+name+'x"', '"'+name+'y"'
else:
xName = 'x%04d' % i
yName = 'y%04d' % i
if appendAllX or i == 0:
header.extend([xName, yName])
else:
header.extend([yName])
if self.params['separator'] == 'comma':
sep = ','
@ -51,12 +61,20 @@ class CSVExporter(Exporter):
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for d in data:
for j in [0, 1]:
if i < len(d[j]):
fd.write(numFormat % d[j][i] + sep)
for j, d in enumerate(data):
# write x value if this is the first column, or if we want x
# for all rows
if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
else:
fd.write(' %s' % sep)
# write y value
if d is not None and i < len(d[1]):
fd.write(numFormat % d[1][i] + sep)
else:
fd.write(' %s' % sep)
fd.write('\n')
fd.close()

View File

@ -0,0 +1,58 @@
from ..Qt import QtGui, QtCore
from .Exporter import Exporter
from ..parametertree import Parameter
from .. import PlotItem
import numpy
try:
import h5py
HAVE_HDF5 = True
except ImportError:
HAVE_HDF5 = False
__all__ = ['HDF5Exporter']
class HDF5Exporter(Exporter):
Name = "HDF5 Export: plot (x,y)"
windows = []
allowCopy = False
def __init__(self, item):
Exporter.__init__(self, item)
self.params = Parameter(name='params', type='group', children=[
{'name': 'Name', 'type': 'str', 'value': 'Export',},
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']},
])
def parameters(self):
return self.params
def export(self, fileName=None):
if not HAVE_HDF5:
raise RuntimeError("This exporter requires the h5py package, "
"but it was not importable.")
if not isinstance(self.item, PlotItem):
raise Exception("Must have a PlotItem selected for HDF5 export.")
if fileName is None:
self.fileSaveDialog(filter=["*.h5", "*.hdf", "*.hd5"])
return
dsname = self.params['Name']
fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite"
data = []
appendAllX = self.params['columnMode'] == '(x,y) per plot'
for i,c in enumerate(self.item.curves):
d = c.getData()
if appendAllX or i == 0:
data.append(d[0])
data.append(d[1])
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fd.close()
if HAVE_HDF5:
HDF5Exporter.register()

View File

@ -4,7 +4,29 @@ from .. import PlotItem
from .. import functions as fn
__all__ = ['MatplotlibExporter']
"""
It is helpful when using the matplotlib Exporter if your
.matplotlib/matplotlibrc file is configured appropriately.
The following are suggested for getting usable PDF output that
can be edited in Illustrator, etc.
backend : Qt4Agg
text.usetex : True # Assumes you have a findable LaTeX installation
interactive : False
font.family : sans-serif
font.sans-serif : 'Arial' # (make first in list)
mathtext.default : sf
figure.facecolor : white # personal preference
# next setting allows pdf font to be readable in Adobe Illustrator
pdf.fonttype : 42 # set fonts to TrueType (otherwise it will be 3
# and the text will be vectorized.
text.dvipnghack : True # primarily to clean up font appearance on Mac
The advantage is that there is less to do to get an exported file cleaned and ready for
publication. Fonts are not vectorized (outlined), and window colors are white.
"""
class MatplotlibExporter(Exporter):
Name = "Matplotlib Window"
@ -14,18 +36,43 @@ class MatplotlibExporter(Exporter):
def parameters(self):
return None
def cleanAxes(self, axl):
if type(axl) is not list:
axl = [axl]
for ax in axl:
if ax is None:
continue
for loc, spine in ax.spines.iteritems():
if loc in ['left', 'bottom']:
pass
elif loc in ['right', 'top']:
spine.set_color('none')
# do not draw the spine
else:
raise ValueError('Unknown spine location: %s' % loc)
# turn off ticks when there is no spine
ax.xaxis.set_ticks_position('bottom')
def export(self, fileName=None):
if isinstance(self.item, PlotItem):
mpw = MatplotlibWindow()
MatplotlibExporter.windows.append(mpw)
stdFont = 'Arial'
fig = mpw.getFigure()
ax = fig.add_subplot(111)
# get labels from the graphic item
xlabel = self.item.axes['bottom']['item'].label.toPlainText()
ylabel = self.item.axes['left']['item'].label.toPlainText()
title = self.item.titleLabel.text
ax = fig.add_subplot(111, title=title)
ax.clear()
self.cleanAxes(ax)
#ax.grid(True)
for item in self.item.curves:
x, y = item.getData()
opts = item.opts
@ -42,17 +89,21 @@ class MatplotlibExporter(Exporter):
symbolBrush = fn.mkBrush(opts['symbolBrush'])
markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())])
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())])
markersize = opts['symbolSize']
if opts['fillLevel'] is not None and opts['fillBrush'] is not None:
fillBrush = fn.mkBrush(opts['fillBrush'])
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())])
ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor)
ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor)
pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(),
linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor,
markersize=markersize)
xr, yr = self.item.viewRange()
ax.set_xbound(*xr)
ax.set_ybound(*yr)
ax.set_xlabel(xlabel) # place the labels.
ax.set_ylabel(ylabel)
mpw.draw()
else:
raise Exception("Matplotlib export currently only works with plot items")

View File

@ -102,14 +102,12 @@ xmlHeader = """\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
<title>pyqtgraph SVG export</title>
<desc>Generated with Qt and pyqtgraph</desc>
<defs>
</defs>
"""
def generateSvg(item):
global xmlHeader
try:
node = _generateItemSvg(item)
node, defs = _generateItemSvg(item)
finally:
## reset export mode for all items in the tree
if isinstance(item, QtGui.QGraphicsScene):
@ -124,7 +122,11 @@ def generateSvg(item):
cleanXml(node)
return xmlHeader + node.toprettyxml(indent=' ') + "\n</svg>\n"
defsXml = "<defs>\n"
for d in defs:
defsXml += d.toprettyxml(indent=' ')
defsXml += "</defs>\n"
return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n</svg>\n"
def _generateItemSvg(item, nodes=None, root=None):
@ -230,6 +232,10 @@ def _generateItemSvg(item, nodes=None, root=None):
g1 = doc.getElementsByTagName('g')[0]
## get list of sub-groups
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
defs = doc.getElementsByTagName('defs')
if len(defs) > 0:
defs = [n for n in defs[0].childNodes if isinstance(n, xml.Element)]
except:
print(doc.toxml())
raise
@ -238,7 +244,7 @@ def _generateItemSvg(item, nodes=None, root=None):
## Get rid of group transformation matrices by applying
## transformation to inner coordinates
correctCoordinates(g1, item)
correctCoordinates(g1, defs, item)
profiler('correct')
## make sure g1 has the transformation matrix
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
@ -275,7 +281,9 @@ def _generateItemSvg(item, nodes=None, root=None):
path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
item.scene().addItem(path)
try:
pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
#pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0]
# assume <defs> for this path is empty.. possibly problematic.
finally:
item.scene().removeItem(path)
@ -294,14 +302,19 @@ def _generateItemSvg(item, nodes=None, root=None):
## Add all child items as sub-elements.
childs.sort(key=lambda c: c.zValue())
for ch in childs:
cg = _generateItemSvg(ch, nodes, root)
if cg is None:
csvg = _generateItemSvg(ch, nodes, root)
if csvg is None:
continue
cg, cdefs = csvg
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
defs.extend(cdefs)
profiler('children')
return g1
return g1, defs
def correctCoordinates(node, item):
def correctCoordinates(node, defs, item):
# TODO: correct gradient coordinates inside defs
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
## Each item is represented by a single top-level group with one or more groups inside.
## Each inner group contains one or more drawing primitives, possibly of different types.

View File

@ -4,7 +4,7 @@ from .SVGExporter import *
from .Matplotlib import *
from .CSVExporter import *
from .PrintExporter import *
from .HDF5Exporter import *
def listExporters():
return Exporter.Exporters[:]

View File

@ -20,41 +20,12 @@ from ..debug import printExc
from .. import configfile as configfile
from .. import dockarea as dockarea
from . import FlowchartGraphicsView
from .. import functions as fn
def strDict(d):
return dict([(str(k), v) for k, v in d.items()])
def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
"""Topological sort. Arguments are:
deps dictionary describing dependencies where a:[b,c] means "a depends on b and c"
nodes optional, specifies list of starting nodes (these should be the nodes
which are not depended on by any other nodes)
"""
if nodes is None:
## run through deps to find nodes that are not depended upon
rem = set()
for dep in deps.values():
rem |= set(dep)
nodes = set(deps.keys()) - rem
if seen is None:
seen = set()
stack = []
sorted = []
#print " "*depth, "Starting from", nodes
for n in nodes:
if n in stack:
raise Exception("Cyclic dependency detected", stack + [n])
if n in seen:
continue
seen.add(n)
#print " "*depth, " descending into", n, deps[n]
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
#print " "*depth, " Added", n
sorted.append(n)
#print " "*depth, " ", sorted
return sorted
class Flowchart(Node):
@ -278,9 +249,10 @@ class Flowchart(Node):
## Record inputs given to process()
for n, t in self.inputNode.outputs().items():
if n not in args:
raise Exception("Parameter %s required to process this chart." % n)
data[t] = args[n]
# if n not in args:
# raise Exception("Parameter %s required to process this chart." % n)
if n in args:
data[t] = args[n]
ret = {}
@ -305,7 +277,7 @@ class Flowchart(Node):
if len(inputs) == 0:
continue
if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs
args[inp.name()] = dict([(i, data[i]) for i in inputs])
args[inp.name()] = dict([(i, data[i]) for i in inputs if i in data])
else: ## single-inputs terminals only need the single input value available
args[inp.name()] = data[inputs[0]]
@ -325,9 +297,8 @@ class Flowchart(Node):
#print out.name()
try:
data[out] = result[out.name()]
except:
print(out, out.name())
raise
except KeyError:
pass
elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory)
#print "===> delete", arg
if arg in data:
@ -352,7 +323,7 @@ class Flowchart(Node):
#print "DEPS:", deps
## determine correct node-processing order
#deps[self] = []
order = toposort(deps)
order = fn.toposort(deps)
#print "ORDER1:", order
## construct list of operations
@ -401,7 +372,7 @@ class Flowchart(Node):
deps[node].extend(t.dependentNodes())
## determine order of updates
order = toposort(deps, nodes=[startNode])
order = fn.toposort(deps, nodes=[startNode])
order.reverse()
## keep track of terminals that have been updated
@ -542,7 +513,7 @@ class Flowchart(Node):
return
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
#fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName)
fileName = unicode(fileName)
state = configfile.readConfigFile(fileName)
self.restoreState(state, clear=True)
self.viewBox.autoRange()
@ -563,7 +534,7 @@ class Flowchart(Node):
self.fileDialog.fileSelected.connect(self.saveFile)
return
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName)
fileName = unicode(fileName)
configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName)
@ -685,7 +656,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def fileSaved(self, fileName):
self.setCurrentFile(str(fileName))
self.setCurrentFile(unicode(fileName))
self.ui.saveBtn.success("Saved.")
def saveClicked(self):
@ -714,7 +685,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def setCurrentFile(self, fileName):
self.currentFileName = str(fileName)
self.currentFileName = unicode(fileName)
if fileName is None:
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
else:

View File

@ -182,8 +182,8 @@ class EvalNode(Node):
def __init__(self, name):
Node.__init__(self, name,
terminals = {
'input': {'io': 'in', 'renamable': True},
'output': {'io': 'out', 'renamable': True},
'input': {'io': 'in', 'renamable': True, 'multiable': True},
'output': {'io': 'out', 'renamable': True, 'multiable': True},
},
allowAddInput=True, allowAddOutput=True)

View File

@ -6,6 +6,8 @@ from ... import functions as pgfn
from .common import *
import numpy as np
from ... import PolyLineROI
from ... import Point
from ... import metaarray as metaarray
@ -201,6 +203,72 @@ class Detrend(CtrlNode):
raise Exception("DetrendFilter node requires the package scipy.signal.")
return detrend(data)
class RemoveBaseline(PlottingCtrlNode):
"""Remove an arbitrary, graphically defined baseline from the data."""
nodeName = 'RemoveBaseline'
def __init__(self, name):
## define inputs and outputs (one output needs to be a plot)
PlottingCtrlNode.__init__(self, name)
self.line = PolyLineROI([[0,0],[1,0]])
self.line.sigRegionChanged.connect(self.changed)
## create a PolyLineROI, add it to a plot -- actually, I think we want to do this after the node is connected to a plot (look at EventDetection.ThresholdEvents node for ideas), and possible after there is data. We will need to update the end positions of the line each time the input data changes
#self.line = None ## will become a PolyLineROI
def connectToPlot(self, node):
"""Define what happens when the node is connected to a plot"""
if node.plot is None:
return
node.getPlot().addItem(self.line)
def disconnectFromPlot(self, plot):
"""Define what happens when the node is disconnected from a plot"""
plot.removeItem(self.line)
def processData(self, data):
## get array of baseline (from PolyLineROI)
h0 = self.line.getHandles()[0]
h1 = self.line.getHandles()[-1]
timeVals = data.xvals(0)
h0.setPos(timeVals[0], h0.pos()[1])
h1.setPos(timeVals[-1], h1.pos()[1])
pts = self.line.listPoints() ## lists line handles in same coordinates as data
pts, indices = self.adjustXPositions(pts, timeVals) ## maxe sure x positions match x positions of data points
## construct an array that represents the baseline
arr = np.zeros(len(data), dtype=float)
n = 1
arr[0] = pts[0].y()
for i in range(len(pts)-1):
x1 = pts[i].x()
x2 = pts[i+1].x()
y1 = pts[i].y()
y2 = pts[i+1].y()
m = (y2-y1)/(x2-x1)
b = y1
times = timeVals[(timeVals > x1)*(timeVals <= x2)]
arr[n:n+len(times)] = (m*(times-times[0]))+b
n += len(times)
return data - arr ## subract baseline from data
def adjustXPositions(self, pts, data):
"""Return a list of Point() where the x position is set to the nearest x value in *data* for each point in *pts*."""
points = []
timeIndices = []
for p in pts:
x = np.argwhere(abs(data - p.x()) == abs(data - p.x()).min())
points.append(Point(data[x], p.y()))
timeIndices.append(x)
return points, timeIndices
class AdaptiveDetrend(CtrlNode):
"""Removes baseline from data, ignoring anomalous events"""
@ -275,4 +343,4 @@ class RemovePeriodic(CtrlNode):
return ma

View File

@ -131,6 +131,42 @@ class CtrlNode(Node):
l.show()
class PlottingCtrlNode(CtrlNode):
"""Abstract class for CtrlNodes that can connect to plots."""
def __init__(self, name, ui=None, terminals=None):
#print "PlottingCtrlNode.__init__ called."
CtrlNode.__init__(self, name, ui=ui, terminals=terminals)
self.plotTerminal = self.addOutput('plot', optional=True)
def connected(self, term, remote):
CtrlNode.connected(self, term, remote)
if term is not self.plotTerminal:
return
node = remote.node()
node.sigPlotChanged.connect(self.connectToPlot)
self.connectToPlot(node)
def disconnected(self, term, remote):
CtrlNode.disconnected(self, term, remote)
if term is not self.plotTerminal:
return
remote.node().sigPlotChanged.disconnect(self.connectToPlot)
self.disconnectFromPlot(remote.node().getPlot())
def connectToPlot(self, node):
"""Define what happens when the node is connected to a plot"""
raise Exception("Must be re-implemented in subclass")
def disconnectFromPlot(self, plot):
"""Define what happens when the node is disconnected from a plot"""
raise Exception("Must be re-implemented in subclass")
def process(self, In, display=True):
out = CtrlNode.process(self, In, display)
out['plot'] = None
return out
def metaArrayWrapper(fn):
def newFn(self, data, *args, **kargs):

View File

@ -206,7 +206,7 @@ def adaptiveDetrend(data, x=None, threshold=3.0):
#d3 = where(mask, 0, d2)
#d4 = d2 - lowPass(d3, cutoffs[1], dt=dt)
lr = stats.linregress(x[mask], d[mask])
lr = scipy.stats.linregress(x[mask], d[mask])
base = lr[1] + lr[0]*x
d4 = d - base

View File

@ -591,6 +591,50 @@ def interpolateArray(data, x, default=0.0):
return result
def subArray(data, offset, shape, stride):
"""
Unpack a sub-array from *data* using the specified offset, shape, and stride.
Note that *stride* is specified in array elements, not bytes.
For example, we have a 2x3 array packed in a 1D array as follows::
data = [_, _, 00, 01, 02, _, 10, 11, 12, _]
Then we can unpack the sub-array with this call::
subArray(data, offset=2, shape=(2, 3), stride=(4, 1))
..which returns::
[[00, 01, 02],
[10, 11, 12]]
This function operates only on the first axis of *data*. So changing
the input in the example above to have shape (10, 7) would cause the
output to have shape (2, 3, 7).
"""
#data = data.flatten()
data = data[offset:]
shape = tuple(shape)
stride = tuple(stride)
extraShape = data.shape[1:]
#print data.shape, offset, shape, stride
for i in range(len(shape)):
mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),)
newShape = shape[:i+1]
if i < len(shape)-1:
newShape += (stride[i],)
newShape += extraShape
#print i, mask, newShape
#print "start:\n", data.shape, data
data = data[mask]
#print "mask:\n", data.shape, data
data = data.reshape(newShape)
#print "reshape:\n", data.shape, data
return data
def transformToArray(tr):
"""
Given a QTransform, return a 3x3 numpy array.
@ -2156,3 +2200,51 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning
def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
"""Topological sort. Arguments are:
deps dictionary describing dependencies where a:[b,c] means "a depends on b and c"
nodes optional, specifies list of starting nodes (these should be the nodes
which are not depended on by any other nodes). Other candidate starting
nodes will be ignored.
Example::
# Sort the following graph:
#
# B ──┬─────> C <── D
# │ │
# E <─┴─> A <─┘
#
deps = {'a': ['b', 'c'], 'c': ['b', 'd'], 'e': ['b']}
toposort(deps)
=> ['b', 'd', 'c', 'a', 'e']
"""
# fill in empty dep lists
deps = deps.copy()
for k,v in list(deps.items()):
for k in v:
if k not in deps:
deps[k] = []
if nodes is None:
## run through deps to find nodes that are not depended upon
rem = set()
for dep in deps.values():
rem |= set(dep)
nodes = set(deps.keys()) - rem
if seen is None:
seen = set()
stack = []
sorted = []
for n in nodes:
if n in stack:
raise Exception("Cyclic dependency detected", stack + [n])
if n in seen:
continue
seen.add(n)
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
sorted.append(n)
return sorted

View File

@ -918,13 +918,17 @@ class AxisItem(GraphicsWidget):
rects.append(br)
textRects.append(rects[-1])
## measure all text, make sure there's enough room
if axis == 0:
textSize = np.sum([r.height() for r in textRects])
textSize2 = np.max([r.width() for r in textRects]) if textRects else 0
if len(textRects) > 0:
## measure all text, make sure there's enough room
if axis == 0:
textSize = np.sum([r.height() for r in textRects])
textSize2 = np.max([r.width() for r in textRects])
else:
textSize = np.sum([r.width() for r in textRects])
textSize2 = np.max([r.height() for r in textRects])
else:
textSize = np.sum([r.width() for r in textRects])
textSize2 = np.max([r.height() for r in textRects]) if textRects else 0
textSize = 0
textSize2 = 0
if i > 0: ## always draw top level
## If the strings are too crowded, stop drawing text now.

View File

@ -3,6 +3,7 @@ from ..python2_3 import sortList
from .. import functions as fn
from .GraphicsObject import GraphicsObject
from .GraphicsWidget import GraphicsWidget
from ..widgets.SpinBox import SpinBox
import weakref
from ..pgcollections import OrderedDict
from ..colormap import ColorMap
@ -300,6 +301,7 @@ class TickSliderItem(GraphicsWidget):
pos.setX(x)
tick.setPos(pos)
self.ticks[tick] = val
self.updateGradient()
def tickValue(self, tick):
## public
@ -537,23 +539,22 @@ class GradientEditorItem(TickSliderItem):
def tickClicked(self, tick, ev):
#private
if ev.button() == QtCore.Qt.LeftButton:
if not tick.colorChangeAllowed:
return
self.currentTick = tick
self.currentTickColor = tick.color
self.colorDialog.setCurrentColor(tick.color)
self.colorDialog.open()
#color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
#if color.isValid():
#self.setTickColor(tick, color)
#self.updateGradient()
self.raiseColorDialog(tick)
elif ev.button() == QtCore.Qt.RightButton:
if not tick.removeAllowed:
return
if len(self.ticks) > 2:
self.removeTick(tick)
self.updateGradient()
self.raiseTickContextMenu(tick, ev)
def raiseColorDialog(self, tick):
if not tick.colorChangeAllowed:
return
self.currentTick = tick
self.currentTickColor = tick.color
self.colorDialog.setCurrentColor(tick.color)
self.colorDialog.open()
def raiseTickContextMenu(self, tick, ev):
self.tickMenu = TickMenu(tick, self)
self.tickMenu.popup(ev.screenPos().toQPoint())
def tickMoved(self, tick, pos):
#private
TickSliderItem.tickMoved(self, tick, pos)
@ -726,6 +727,7 @@ class GradientEditorItem(TickSliderItem):
def removeTick(self, tick, finish=True):
TickSliderItem.removeTick(self, tick)
if finish:
self.updateGradient()
self.sigGradientChangeFinished.emit(self)
@ -867,44 +869,59 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO
self.currentPen = self.pen
self.update()
#def mouseMoveEvent(self, ev):
##print self, "move", ev.scenePos()
#if not self.movable:
#return
#if not ev.buttons() & QtCore.Qt.LeftButton:
#return
#newPos = ev.scenePos() + self.mouseOffset
#newPos.setY(self.pos().y())
##newPos.setX(min(max(newPos.x(), 0), 100))
#self.setPos(newPos)
#self.view().tickMoved(self, newPos)
#self.movedSincePress = True
##self.emit(QtCore.SIGNAL('tickChanged'), self)
#ev.accept()
#def mousePressEvent(self, ev):
#self.movedSincePress = False
#if ev.button() == QtCore.Qt.LeftButton:
#ev.accept()
#self.mouseOffset = self.pos() - ev.scenePos()
#self.pressPos = ev.scenePos()
#elif ev.button() == QtCore.Qt.RightButton:
#ev.accept()
##if self.endTick:
##return
##self.view.tickChanged(self, delete=True)
#def mouseReleaseEvent(self, ev):
##print self, "release", ev.scenePos()
#if not self.movedSincePress:
#self.view().tickClicked(self, ev)
class TickMenu(QtGui.QMenu):
def __init__(self, tick, sliderItem):
QtGui.QMenu.__init__(self)
##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos:
##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
##if color.isValid():
##self.color = color
##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color)))
###self.emit(QtCore.SIGNAL('tickChanged'), self)
##self.view.tickChanged(self)
self.tick = weakref.ref(tick)
self.sliderItem = weakref.ref(sliderItem)
self.removeAct = self.addAction("Remove Tick", lambda: self.sliderItem().removeTick(tick))
if (not self.tick().removeAllowed) or len(self.sliderItem().ticks) < 3:
self.removeAct.setEnabled(False)
positionMenu = self.addMenu("Set Position")
w = QtGui.QWidget()
l = QtGui.QGridLayout()
w.setLayout(l)
value = sliderItem.tickValue(tick)
self.fracPosSpin = SpinBox()
self.fracPosSpin.setOpts(value=value, bounds=(0.0, 1.0), step=0.01, decimals=2)
#self.dataPosSpin = SpinBox(value=dataVal)
#self.dataPosSpin.setOpts(decimals=3, siPrefix=True)
l.addWidget(QtGui.QLabel("Position:"), 0,0)
l.addWidget(self.fracPosSpin, 0, 1)
#l.addWidget(QtGui.QLabel("Position (data units):"), 1, 0)
#l.addWidget(self.dataPosSpin, 1,1)
#if self.sliderItem().dataParent is None:
# self.dataPosSpin.setEnabled(False)
a = QtGui.QWidgetAction(self)
a.setDefaultWidget(w)
positionMenu.addAction(a)
self.fracPosSpin.sigValueChanging.connect(self.fractionalValueChanged)
#self.dataPosSpin.valueChanged.connect(self.dataValueChanged)
colorAct = self.addAction("Set Color", lambda: self.sliderItem().raiseColorDialog(self.tick()))
if not self.tick().colorChangeAllowed:
colorAct.setEnabled(False)
def fractionalValueChanged(self, x):
self.sliderItem().setTickValue(self.tick(), self.fracPosSpin.value())
#if self.sliderItem().dataParent is not None:
# self.dataPosSpin.blockSignals(True)
# self.dataPosSpin.setValue(self.sliderItem().tickDataValue(self.tick()))
# self.dataPosSpin.blockSignals(False)
#def dataValueChanged(self, val):
# self.sliderItem().setTickValue(self.tick(), val, dataUnits=True)
# self.fracPosSpin.blockSignals(True)
# self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick()))
# self.fracPosSpin.blockSignals(False)

View File

@ -177,6 +177,12 @@ class ImageItem(GraphicsObject):
self.translate(rect.left(), rect.top())
self.scale(rect.width() / self.width(), rect.height() / self.height())
def clear(self):
self.image = None
self.prepareGeometryChange()
self.informViewBoundsChanged()
self.update()
def setImage(self, image=None, autoLevels=None, **kargs):
"""
Update the image displayed by this item. For more information on how the image
@ -512,6 +518,9 @@ class ImageItem(GraphicsObject):
def removeClicked(self):
## Send remove event only after we have exited the menu event handler
self.removeTimer = QtCore.QTimer()
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self))
self.removeTimer.timeout.connect(self.emitRemoveRequested)
self.removeTimer.start(0)
def emitRemoveRequested(self):
self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
self.sigRemoveRequested.emit(self)

View File

@ -168,6 +168,7 @@ class PlotDataItem(GraphicsObject):
'downsample': 1,
'autoDownsample': False,
'downsampleMethod': 'peak',
'autoDownsampleFactor': 5., # draw ~5 samples per pixel
'clipToView': False,
'data': None,
@ -380,14 +381,23 @@ class PlotDataItem(GraphicsObject):
elif len(args) == 2:
seq = ('listOfValues', 'MetaArray', 'empty')
if dataType(args[0]) not in seq or dataType(args[1]) not in seq:
dtyp = dataType(args[0]), dataType(args[1])
if dtyp[0] not in seq or dtyp[1] not in seq:
raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1]))))
if not isinstance(args[0], np.ndarray):
x = np.array(args[0])
#x = np.array(args[0])
if dtyp[0] == 'MetaArray':
x = args[0].asarray()
else:
x = np.array(args[0])
else:
x = args[0].view(np.ndarray)
if not isinstance(args[1], np.ndarray):
y = np.array(args[1])
#y = np.array(args[1])
if dtyp[1] == 'MetaArray':
y = args[1].asarray()
else:
y = np.array(args[1])
else:
y = args[1].view(np.ndarray)
@ -538,7 +548,7 @@ class PlotDataItem(GraphicsObject):
x1 = (range.right()-x[0]) / dx
width = self.getViewBox().width()
if width != 0.0:
ds = int(max(1, int(0.2 * (x1-x0) / width)))
ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor']))))
## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']:

View File

@ -469,7 +469,8 @@ class PlotItem(GraphicsWidget):
### Average data together
(x, y) = curve.getData()
if plot.yData is not None:
if plot.yData is not None and y.shape == plot.yData.shape:
# note that if shapes do not match, then the average resets.
newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n)
plot.setData(plot.xData, newData)
else:
@ -1207,10 +1208,13 @@ class PlotItem(GraphicsWidget):
self.updateButtons()
def updateButtons(self):
if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()):
self.autoBtn.show()
else:
self.autoBtn.hide()
try:
if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()):
self.autoBtn.show()
else:
self.autoBtn.hide()
except RuntimeError:
pass # this can happen if the plot has been deleted.
def _plotArray(self, arr, x=None, **kargs):
if arr.ndim != 1:

View File

@ -25,7 +25,7 @@ from .UIGraphicsItem import UIGraphicsItem
__all__ = [
'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI',
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI',
]
@ -862,8 +862,10 @@ class ROI(GraphicsObject):
elif h['type'] == 'sr':
if h['center'][0] == h['pos'][0]:
scaleAxis = 1
nonScaleAxis=0
else:
scaleAxis = 0
nonScaleAxis=1
try:
if lp1.length() == 0 or lp0.length() == 0:
@ -885,6 +887,8 @@ class ROI(GraphicsObject):
newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize
if newState['size'][scaleAxis] == 0:
newState['size'][scaleAxis] = 1
if self.aspectLocked:
newState['size'][nonScaleAxis] = newState['size'][scaleAxis]
c1 = c * newState['size']
tr = QtGui.QTransform()
@ -972,14 +976,16 @@ class ROI(GraphicsObject):
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
def paint(self, p, opt, widget):
p.save()
r = self.boundingRect()
# p.save()
# Note: don't use self.boundingRect here, because subclasses may need to redefine it.
r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
p.translate(r.left(), r.top())
p.scale(r.width(), r.height())
p.drawRect(0, 0, 1, 1)
p.restore()
# p.restore()
def getArraySlice(self, data, img, axes=(0,1), returnSlice=True):
"""Return a tuple of slice objects that can be used to slice the region from data covered by this ROI.
@ -2139,6 +2145,102 @@ class SpiralROI(ROI):
p.drawRect(self.boundingRect())
class CrosshairROI(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, **kargs):
if size == None:
#size = [100e-6,100e-6]
size=[1,1]
if pos == None:
pos = [0,0]
self._shape = None
ROI.__init__(self, pos, size, **kargs)
self.sigRegionChanged.connect(self.invalidate)
self.addScaleRotateHandle(Point(1, 0), Point(0, 0))
self.aspectLocked = True
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
def boundingRect(self):
#size = self.size()
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
return self.shape().boundingRect()
#def getRect(self):
### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses
#size = self.size()
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
def shape(self):
if self._shape is None:
radius = self.getState()['size'][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
p.moveTo(Point(-radius, 0))
p.lineTo(Point(radius, 0))
p = self.mapToDevice(p)
stroker = QtGui.QPainterPathStroker()
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline)
##h1 = self.handles[0]['item'].pos()
##h2 = self.handles[1]['item'].pos()
#w1 = Point(-0.5, 0)*self.size()
#w2 = Point(0.5, 0)*self.size()
#h1 = Point(0, -0.5)*self.size()
#h2 = Point(0, 0.5)*self.size()
#dh = h2-h1
#dw = w2-w1
#if dh.length() == 0 or dw.length() == 0:
#return p
#pxv = self.pixelVectors(dh)[1]
#if pxv is None:
#return p
#pxv *= 4
#p.moveTo(h1+pxv)
#p.lineTo(h2+pxv)
#p.lineTo(h2-pxv)
#p.lineTo(h1-pxv)
#p.lineTo(h1+pxv)
#pxv = self.pixelVectors(dw)[1]
#if pxv is None:
#return p
#pxv *= 4
#p.moveTo(w1+pxv)
#p.lineTo(w2+pxv)
#p.lineTo(w2-pxv)
#p.lineTo(w1-pxv)
#p.lineTo(w1+pxv)
return self._shape
def paint(self, p, *args):
#p.save()
#r = self.getRect()
radius = self.getState()['size'][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
#p.translate(r.left(), r.top())
#p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5
#p.drawLine(0,5, 10,5)
#p.drawLine(5,0, 5,10)
#p.restore()
p.drawLine(Point(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0))

View File

@ -5,6 +5,7 @@ from .TextItem import TextItem
import numpy as np
from .. import functions as fn
from .. import getConfigOption
from ..Point import Point
__all__ = ['ScaleBar']
@ -12,7 +13,7 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
"""
Displays a rectangular bar to indicate the relative scale of objects on the view.
"""
def __init__(self, size, width=5, brush=None, pen=None, suffix='m'):
def __init__(self, size, width=5, brush=None, pen=None, suffix='m', offset=None):
GraphicsObject.__init__(self)
GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemHasNoContents)
@ -24,6 +25,9 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
self.pen = fn.mkPen(pen)
self._width = width
self.size = size
if offset == None:
offset = (0,0)
self.offset = offset
self.bar = QtGui.QGraphicsRectItem()
self.bar.setPen(self.pen)
@ -54,51 +58,14 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
def boundingRect(self):
return QtCore.QRectF()
def setParentItem(self, p):
ret = GraphicsObject.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
#class ScaleBar(UIGraphicsItem):
#"""
#Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
#"""
#def __init__(self, size, width=5, color=(100, 100, 255)):
#UIGraphicsItem.__init__(self)
#self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
#self.brush = fn.mkBrush(color)
#self.pen = fn.mkPen((0,0,0))
#self._width = width
#self.size = size
#def paint(self, p, opt, widget):
#UIGraphicsItem.paint(self, p, opt, widget)
#rect = self.boundingRect()
#unit = self.pixelSize()
#y = rect.top() + (rect.bottom()-rect.top()) * 0.02
#y1 = y + unit[1]*self._width
#x = rect.right() + (rect.left()-rect.right()) * 0.02
#x1 = x - self.size
#p.setPen(self.pen)
#p.setBrush(self.brush)
#rect = QtCore.QRectF(
#QtCore.QPointF(x1, y1),
#QtCore.QPointF(x, y)
#)
#p.translate(x1, y1)
#p.scale(rect.width(), rect.height())
#p.drawRect(0, 0, 1, 1)
#alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
#p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
#for i in range(1, 10):
##x2 = x + (x1-x) * 0.1 * i
#x2 = 0.1 * i
#p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
#def setSize(self, s):
#self.size = s

View File

@ -68,10 +68,12 @@ def renderSymbol(symbol, size, pen, brush, device=None):
device = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
device.fill(0)
p = QtGui.QPainter(device)
p.setRenderHint(p.Antialiasing)
p.translate(device.width()*0.5, device.height()*0.5)
drawSymbol(p, symbol, size, pen, brush)
p.end()
try:
p.setRenderHint(p.Antialiasing)
p.translate(device.width()*0.5, device.height()*0.5)
drawSymbol(p, symbol, size, pen, brush)
finally:
p.end()
return device
def makeSymbolPixmap(size, pen, brush, symbol):

View File

@ -760,7 +760,8 @@ class ViewBox(GraphicsWidget):
x = vr.left()+x, vr.right()+x
if y is not None:
y = vr.top()+y, vr.bottom()+y
self.setRange(xRange=x, yRange=y, padding=0)
if x is not None or y is not None:
self.setRange(xRange=x, yRange=y, padding=0)
@ -902,6 +903,14 @@ class ViewBox(GraphicsWidget):
return
args['padding'] = 0
args['disableAutoRange'] = False
# check for and ignore bad ranges
for k in ['xRange', 'yRange']:
if k in args:
if not np.all(np.isfinite(args[k])):
r = args.pop(k)
print "Warning: %s is invalid: %s" % (k, str(r))
self.setRange(**args)
finally:
self._autoRangeNeedsUpdate = False
@ -1066,7 +1075,7 @@ class ViewBox(GraphicsWidget):
return
self.state['yInverted'] = b
#self.updateMatrix(changed=(False, True))
self._matrixNeedsUpdate = True # updateViewRange won't detect this for us
self.updateViewRange()
self.sigStateChanged.emit(self)
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
@ -1485,7 +1494,7 @@ class ViewBox(GraphicsWidget):
aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect()
bounds = self.rect()
if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0:
if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]:
## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
@ -1581,18 +1590,16 @@ class ViewBox(GraphicsWidget):
if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange'])
self.update()
self._matrixNeedsUpdate = True
# Inform linked views that the range has changed
for ax in [0, 1]:
if not changed[ax]:
continue
link = self.linkedView(ax)
if link is not None:
link.linkedViewChanged(self, ax)
# Inform linked views that the range has changed
for ax in [0, 1]:
if not changed[ax]:
continue
link = self.linkedView(ax)
if link is not None:
link.linkedViewChanged(self, ax)
self.update()
self._matrixNeedsUpdate = True
def updateMatrix(self, changed=None):
## Make the childGroup's transform match the requested viewRange.
bounds = self.rect()

View File

@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features:
- ROI plotting
- Image normalization through a variety of methods
"""
import sys
import os, sys
import numpy as np
from ..Qt import QtCore, QtGui, USE_PYSIDE
@ -136,6 +136,8 @@ class ImageView(QtGui.QWidget):
self.ui.histogram.setImageItem(self.imageItem)
self.menu = None
self.ui.normGroup.hide()
self.roi = PlotROI(10)
@ -176,7 +178,8 @@ class ImageView(QtGui.QWidget):
self.timeLine.sigPositionChanged.connect(self.timeLineChanged)
self.ui.roiBtn.clicked.connect(self.roiClicked)
self.roi.sigRegionChanged.connect(self.roiChanged)
self.ui.normBtn.toggled.connect(self.normToggled)
#self.ui.normBtn.toggled.connect(self.normToggled)
self.ui.menuBtn.clicked.connect(self.menuClicked)
self.ui.normDivideRadio.clicked.connect(self.normRadioChanged)
self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged)
self.ui.normOffRadio.clicked.connect(self.normRadioChanged)
@ -321,6 +324,10 @@ class ImageView(QtGui.QWidget):
profiler()
def clear(self):
self.image = None
self.imageItem.clear()
def play(self, rate):
"""Begin automatically stepping frames forward at the given rate (in fps).
This can also be accessed by pressing the spacebar."""
@ -671,3 +678,43 @@ class ImageView(QtGui.QWidget):
def getHistogramWidget(self):
"""Return the HistogramLUTWidget for this ImageView"""
return self.ui.histogram
def export(self, fileName):
"""
Export data from the ImageView to a file, or to a stack of files if
the data is 3D. Saving an image stack will result in index numbers
being added to the file name. Images are saved as they would appear
onscreen, with levels and lookup table applied.
"""
img = self.getProcessedImage()
if self.hasTimeAxis():
base, ext = os.path.splitext(fileName)
fmt = "%%s%%0%dd%%s" % int(np.log10(img.shape[0])+1)
for i in range(img.shape[0]):
self.imageItem.setImage(img[i], autoLevels=False)
self.imageItem.save(fmt % (base, i, ext))
self.updateImage()
else:
self.imageItem.save(fileName)
def exportClicked(self):
fileName = QtGui.QFileDialog.getSaveFileName()
if fileName == '':
return
self.export(fileName)
def buildMenu(self):
self.menu = QtGui.QMenu()
self.normAction = QtGui.QAction("Normalization", self.menu)
self.normAction.setCheckable(True)
self.normAction.toggled.connect(self.normToggled)
self.menu.addAction(self.normAction)
self.exportAction = QtGui.QAction("Export", self.menu)
self.exportAction.triggered.connect(self.exportClicked)
self.menu.addAction(self.exportAction)
def menuClicked(self):
if self.menu is None:
self.buildMenu()
self.menu.popup(QtGui.QCursor.pos())

View File

@ -53,7 +53,7 @@
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="normBtn">
<widget class="QPushButton" name="menuBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -61,10 +61,7 @@
</sizepolicy>
</property>
<property name="text">
<string>Norm</string>
</property>
<property name="checkable">
<bool>true</bool>
<string>Menu</string>
</property>
</widget>
</item>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui'
# Form implementation generated from reading ui file 'ImageViewTemplate.ui'
#
# Created: Mon Dec 23 10:10:52 2013
# by: PyQt4 UI code generator 4.10
# Created: Thu May 1 15:20:40 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@ -55,15 +55,14 @@ class Ui_Form(object):
self.roiBtn.setCheckable(True)
self.roiBtn.setObjectName(_fromUtf8("roiBtn"))
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1)
self.normBtn = QtGui.QPushButton(self.layoutWidget)
self.menuBtn = QtGui.QPushButton(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth())
self.normBtn.setSizePolicy(sizePolicy)
self.normBtn.setCheckable(True)
self.normBtn.setObjectName(_fromUtf8("normBtn"))
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
self.menuBtn.setSizePolicy(sizePolicy)
self.menuBtn.setObjectName(_fromUtf8("menuBtn"))
self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
self.roiPlot = PlotWidget(self.splitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@ -149,7 +148,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
self.roiBtn.setText(_translate("Form", "ROI", None))
self.normBtn.setText(_translate("Form", "Norm", None))
self.menuBtn.setText(_translate("Form", "Menu", None))
self.normGroup.setTitle(_translate("Form", "Normalization", None))
self.normSubtractRadio.setText(_translate("Form", "Subtract", None))
self.normDivideRadio.setText(_translate("Form", "Divide", None))

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui'
# Form implementation generated from reading ui file 'ImageViewTemplate.ui'
#
# Created: Mon Dec 23 10:10:52 2013
# by: pyside-uic 0.2.14 running on PySide 1.1.2
# Created: Thu May 1 15:20:42 2014
# by: pyside-uic 0.2.15 running on PySide 1.2.1
#
# WARNING! All changes made in this file will be lost!
@ -41,15 +41,14 @@ class Ui_Form(object):
self.roiBtn.setCheckable(True)
self.roiBtn.setObjectName("roiBtn")
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1)
self.normBtn = QtGui.QPushButton(self.layoutWidget)
self.menuBtn = QtGui.QPushButton(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth())
self.normBtn.setSizePolicy(sizePolicy)
self.normBtn.setCheckable(True)
self.normBtn.setObjectName("normBtn")
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
self.menuBtn.setSizePolicy(sizePolicy)
self.menuBtn.setObjectName("menuBtn")
self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
self.roiPlot = PlotWidget(self.splitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@ -135,7 +134,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8))
self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8))
self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8))
self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8))
self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8))
self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -103,6 +103,14 @@ class MetaArray(object):
"""
version = '2'
# Default hdf5 compression to use when writing
# 'gzip' is widely available and somewhat slow
# 'lzf' is faster, but generally not available outside h5py
# 'szip' is also faster, but lacks write support on windows
# (so by default, we use no compression)
# May also be a tuple (filter, opts), such as ('gzip', 3)
defaultCompression = None
## Types allowed as axis or column names
nameTypes = [basestring, tuple]
@ -122,7 +130,7 @@ class MetaArray(object):
if file is not None:
self._data = None
self.readFile(file, **kwargs)
if self._data is None:
if kwargs.get("readAllData", True) and self._data is None:
raise Exception("File read failed: %s" % file)
else:
self._info = info
@ -720,25 +728,28 @@ class MetaArray(object):
"""
## decide which read function to use
fd = open(filename, 'rb')
magic = fd.read(8)
if magic == '\x89HDF\r\n\x1a\n':
fd.close()
self._readHDF5(filename, **kwargs)
self._isHDF = True
else:
fd.seek(0)
meta = MetaArray._readMeta(fd)
if 'version' in meta:
ver = meta['version']
with open(filename, 'rb') as fd:
magic = fd.read(8)
if magic == '\x89HDF\r\n\x1a\n':
fd.close()
self._readHDF5(filename, **kwargs)
self._isHDF = True
else:
ver = 1
rFuncName = '_readData%s' % str(ver)
if not hasattr(MetaArray, rFuncName):
raise Exception("This MetaArray library does not support array version '%s'" % ver)
rFunc = getattr(self, rFuncName)
rFunc(fd, meta, **kwargs)
self._isHDF = False
fd.seek(0)
meta = MetaArray._readMeta(fd)
if not kwargs.get("readAllData", True):
self._data = np.empty(meta['shape'], dtype=meta['type'])
if 'version' in meta:
ver = meta['version']
else:
ver = 1
rFuncName = '_readData%s' % str(ver)
if not hasattr(MetaArray, rFuncName):
raise Exception("This MetaArray library does not support array version '%s'" % ver)
rFunc = getattr(self, rFuncName)
rFunc(fd, meta, **kwargs)
self._isHDF = False
@staticmethod
def _readMeta(fd):
@ -756,7 +767,7 @@ class MetaArray(object):
#print ret
return ret
def _readData1(self, fd, meta, mmap=False):
def _readData1(self, fd, meta, mmap=False, **kwds):
## Read array data from the file descriptor for MetaArray v1 files
## read in axis values for any axis that specifies a length
frameSize = 1
@ -766,16 +777,18 @@ class MetaArray(object):
frameSize *= ax['values_len']
del ax['values_len']
del ax['values_type']
self._info = meta['info']
if not kwds.get("readAllData", True):
return
## the remaining data is the actual array
if mmap:
subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape'])
else:
subarr = np.fromstring(fd.read(), dtype=meta['type'])
subarr.shape = meta['shape']
self._info = meta['info']
self._data = subarr
def _readData2(self, fd, meta, mmap=False, subset=None):
def _readData2(self, fd, meta, mmap=False, subset=None, **kwds):
## read in axis values
dynAxis = None
frameSize = 1
@ -792,7 +805,10 @@ class MetaArray(object):
frameSize *= ax['values_len']
del ax['values_len']
del ax['values_type']
self._info = meta['info']
if not kwds.get("readAllData", True):
return
## No axes are dynamic, just read the entire array in at once
if dynAxis is None:
#if rewriteDynamic is not None:
@ -1027,10 +1043,18 @@ class MetaArray(object):
def writeHDF5(self, fileName, **opts):
## default options for writing datasets
comp = self.defaultCompression
if isinstance(comp, tuple):
comp, copts = comp
else:
copts = None
dsOpts = {
'compression': 'lzf',
'compression': comp,
'chunks': True,
}
if copts is not None:
dsOpts['compression_opts'] = copts
## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending)
appAxis = opts.get('appendAxis', None)

View File

@ -1,5 +1,6 @@
import os, time, sys, traceback, weakref
import numpy as np
import threading
try:
import __builtin__ as builtins
import cPickle as pickle
@ -53,8 +54,10 @@ class RemoteEventHandler(object):
## status is either 'result' or 'error'
## if 'error', then result will be (exception, formatted exceprion)
## where exception may be None if it could not be passed through the Connection.
self.resultLock = threading.RLock()
self.proxies = {} ## maps {weakref(proxy): proxyId}; used to inform the remote process when a proxy has been deleted.
self.proxyLock = threading.RLock()
## attributes that affect the behavior of the proxy.
## See ObjectProxy._setProxyOptions for description
@ -66,10 +69,15 @@ class RemoteEventHandler(object):
'deferGetattr': False, ## True, False
'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ],
}
self.optsLock = threading.RLock()
self.nextRequestId = 0
self.exited = False
# Mutexes to help prevent issues when multiple threads access the same RemoteEventHandler
self.processLock = threading.RLock()
self.sendLock = threading.RLock()
RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid
@classmethod
@ -86,46 +94,59 @@ class RemoteEventHandler(object):
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
def getProxyOption(self, opt):
return self.proxyOptions[opt]
with self.optsLock:
return self.proxyOptions[opt]
def setProxyOptions(self, **kwds):
"""
Set the default behavior options for object proxies.
See ObjectProxy._setProxyOptions for more info.
"""
self.proxyOptions.update(kwds)
with self.optsLock:
self.proxyOptions.update(kwds)
def processRequests(self):
"""Process all pending requests from the pipe, return
after no more events are immediately available. (non-blocking)
Returns the number of events processed.
"""
if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError()
numProcessed = 0
while self.conn.poll():
try:
self.handleRequest()
numProcessed += 1
except ClosedError:
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True
raise
#except IOError as err: ## let handleRequest take care of this.
#self.debugMsg(' got IOError from handleRequest; try again.')
#if err.errno == 4: ## interrupted system call; try again
#continue
#else:
#raise
except:
print("Error in process %s" % self.name)
sys.excepthook(*sys.exc_info())
if numProcessed > 0:
self.debugMsg('processRequests: finished %d requests' % numProcessed)
return numProcessed
with self.processLock:
if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError()
numProcessed = 0
while self.conn.poll():
#try:
#poll = self.conn.poll()
#if not poll:
#break
#except IOError: # this can happen if the remote process dies.
## might it also happen in other circumstances?
#raise ClosedError()
try:
self.handleRequest()
numProcessed += 1
except ClosedError:
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True
raise
#except IOError as err: ## let handleRequest take care of this.
#self.debugMsg(' got IOError from handleRequest; try again.')
#if err.errno == 4: ## interrupted system call; try again
#continue
#else:
#raise
except:
print("Error in process %s" % self.name)
sys.excepthook(*sys.exc_info())
if numProcessed > 0:
self.debugMsg('processRequests: finished %d requests' % numProcessed)
return numProcessed
def handleRequest(self):
"""Handle a single request from the remote process.
@ -183,9 +204,11 @@ class RemoteEventHandler(object):
returnType = opts.get('returnType', 'auto')
if cmd == 'result':
self.results[resultId] = ('result', opts['result'])
with self.resultLock:
self.results[resultId] = ('result', opts['result'])
elif cmd == 'error':
self.results[resultId] = ('error', (opts['exception'], opts['excString']))
with self.resultLock:
self.results[resultId] = ('error', (opts['exception'], opts['excString']))
elif cmd == 'getObjAttr':
result = getattr(opts['obj'], opts['attr'])
elif cmd == 'callObj':
@ -259,7 +282,9 @@ class RemoteEventHandler(object):
self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result)))
#print "returnValue:", returnValue, result
if returnType == 'auto':
result = self.autoProxy(result, self.proxyOptions['noProxyTypes'])
with self.optsLock:
noProxyTypes = self.proxyOptions['noProxyTypes']
result = self.autoProxy(result, noProxyTypes)
elif returnType == 'proxy':
result = LocalObjectProxy(result)
@ -378,54 +403,59 @@ class RemoteEventHandler(object):
traceback
============= =====================================================================
"""
#if len(kwds) > 0:
#print "Warning: send() ignored args:", kwds
if self.exited:
self.debugMsg(' send: exited already; raise ClosedError.')
raise ClosedError()
with self.sendLock:
#if len(kwds) > 0:
#print "Warning: send() ignored args:", kwds
if opts is None:
opts = {}
if opts is None:
opts = {}
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
if reqId is None:
if callSync != 'off': ## requested return value; use the next available request ID
reqId = self.nextRequestId
self.nextRequestId += 1
else:
## If requestId is provided, this _must_ be a response to a previously received request.
assert request in ['result', 'error']
if returnType is not None:
opts['returnType'] = returnType
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
if reqId is None:
if callSync != 'off': ## requested return value; use the next available request ID
reqId = self.nextRequestId
self.nextRequestId += 1
else:
## If requestId is provided, this _must_ be a response to a previously received request.
assert request in ['result', 'error']
#print os.getpid(), "send request:", request, reqId, opts
## double-pickle args to ensure that at least status and request ID get through
try:
optStr = pickle.dumps(opts)
except:
print("==== Error pickling this object: ====")
print(opts)
print("=======================================")
raise
nByteMsgs = 0
if byteData is not None:
nByteMsgs = len(byteData)
if returnType is not None:
opts['returnType'] = returnType
#print os.getpid(), "send request:", request, reqId, opts
## double-pickle args to ensure that at least status and request ID get through
try:
optStr = pickle.dumps(opts)
except:
print("==== Error pickling this object: ====")
print(opts)
print("=======================================")
raise
nByteMsgs = 0
if byteData is not None:
nByteMsgs = len(byteData)
## Send primary request
request = (request, reqId, nByteMsgs, optStr)
self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts)))
self.conn.send(request)
## follow up by sending byte messages
if byteData is not None:
for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages!
self.conn.send_bytes(obj)
self.debugMsg(' sent %d byte messages' % len(byteData))
self.debugMsg(' call sync: %s' % callSync)
if callSync == 'off':
return
## Send primary request
request = (request, reqId, nByteMsgs, optStr)
self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts)))
self.conn.send(request)
## follow up by sending byte messages
if byteData is not None:
for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages!
self.conn.send_bytes(obj)
self.debugMsg(' sent %d byte messages' % len(byteData))
self.debugMsg(' call sync: %s' % callSync)
if callSync == 'off':
return
req = Request(self, reqId, description=str(request), timeout=timeout)
if callSync == 'async':
return req
@ -437,20 +467,30 @@ class RemoteEventHandler(object):
return req
def close(self, callSync='off', noCleanup=False, **kwds):
self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds)
try:
self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds)
self.exited = True
except ClosedError:
pass
def getResult(self, reqId):
## raises NoResultError if the result is not available yet
#print self.results.keys(), os.getpid()
if reqId not in self.results:
with self.resultLock:
haveResult = reqId in self.results
if not haveResult:
try:
self.processRequests()
except ClosedError: ## even if remote connection has closed, we may have
## received new data during this call to processRequests()
pass
if reqId not in self.results:
raise NoResultError()
status, result = self.results.pop(reqId)
with self.resultLock:
if reqId not in self.results:
raise NoResultError()
status, result = self.results.pop(reqId)
if status == 'result':
return result
elif status == 'error':
@ -494,11 +534,13 @@ class RemoteEventHandler(object):
args = list(args)
## Decide whether to send arguments by value or by proxy
noProxyTypes = opts.pop('noProxyTypes', None)
if noProxyTypes is None:
noProxyTypes = self.proxyOptions['noProxyTypes']
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
with self.optsLock:
noProxyTypes = opts.pop('noProxyTypes', None)
if noProxyTypes is None:
noProxyTypes = self.proxyOptions['noProxyTypes']
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
if autoProxy is True:
args = [self.autoProxy(v, noProxyTypes) for v in args]
for k, v in kwds.iteritems():
@ -520,11 +562,14 @@ class RemoteEventHandler(object):
return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts)
def registerProxy(self, proxy):
ref = weakref.ref(proxy, self.deleteProxy)
self.proxies[ref] = proxy._proxyId
with self.proxyLock:
ref = weakref.ref(proxy, self.deleteProxy)
self.proxies[ref] = proxy._proxyId
def deleteProxy(self, ref):
proxyId = self.proxies.pop(ref)
with self.proxyLock:
proxyId = self.proxies.pop(ref)
try:
self.send(request='del', opts=dict(proxyId=proxyId), callSync='off')
except IOError: ## if remote process has closed down, there is no need to send delete requests anymore

View File

@ -1,6 +1,7 @@
from ..Qt import QtGui, QtCore
import os, weakref, re
from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode
from .ParameterItem import ParameterItem
PARAM_TYPES = {}
@ -13,7 +14,9 @@ def registerParameterType(name, cls, override=False):
PARAM_TYPES[name] = cls
PARAM_NAMES[cls] = name
def __reload__(old):
PARAM_TYPES.update(old.get('PARAM_TYPES', {}))
PARAM_NAMES.update(old.get('PARAM_NAMES', {}))
class Parameter(QtCore.QObject):
"""
@ -46,6 +49,7 @@ class Parameter(QtCore.QObject):
including during editing.
sigChildAdded(self, child, index) Emitted when a child is added
sigChildRemoved(self, child) Emitted when a child is removed
sigRemoved(self) Emitted when this parameter is removed
sigParentChanged(self, parent) Emitted when this parameter's parent has changed
sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed
sigDefaultChanged(self, default) Emitted when this parameter's default value has changed
@ -61,6 +65,7 @@ class Parameter(QtCore.QObject):
sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index
sigChildRemoved = QtCore.Signal(object, object) ## self, child
sigRemoved = QtCore.Signal(object) ## self
sigParentChanged = QtCore.Signal(object, object) ## self, parent
sigLimitsChanged = QtCore.Signal(object, object) ## self, limits
sigDefaultChanged = QtCore.Signal(object, object) ## self, default
@ -133,6 +138,12 @@ class Parameter(QtCore.QObject):
expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be
visible). (default=True)
title (str or None) If specified, then the parameter will be
displayed to the user using this string as its name.
However, the parameter will still be referred to
internally using the *name* specified above. Note that
this option is not compatible with renamable=True.
(default=None; added in version 0.9.9)
======================= =========================================================
"""
@ -148,6 +159,7 @@ class Parameter(QtCore.QObject):
'removable': False,
'strictNaming': False, # forces name to be usable as a python variable
'expanded': True,
'title': None,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
}
self.opts.update(opts)
@ -266,16 +278,27 @@ class Parameter(QtCore.QObject):
vals[ch.name()] = (ch.value(), ch.getValues())
return vals
def saveState(self):
def saveState(self, filter=None):
"""
Return a structure representing the entire state of the parameter tree.
The tree state may be restored from this structure using restoreState()
The tree state may be restored from this structure using restoreState().
If *filter* is set to 'user', then only user-settable data will be included in the
returned state.
"""
state = self.opts.copy()
state['children'] = OrderedDict([(ch.name(), ch.saveState()) for ch in self])
if state['type'] is None:
global PARAM_NAMES
state['type'] = PARAM_NAMES.get(type(self), None)
if filter is None:
state = self.opts.copy()
if state['type'] is None:
global PARAM_NAMES
state['type'] = PARAM_NAMES.get(type(self), None)
elif filter == 'user':
state = {'value': self.value()}
else:
raise ValueError("Unrecognized filter argument: '%s'" % filter)
ch = OrderedDict([(ch.name(), ch.saveState(filter=filter)) for ch in self])
if len(ch) > 0:
state['children'] = ch
return state
def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True):
@ -293,8 +316,11 @@ class Parameter(QtCore.QObject):
## list of children may be stored either as list or dict.
if isinstance(childState, dict):
childState = childState.values()
cs = []
for k,v in childState.items():
cs.append(v.copy())
cs[-1].setdefault('name', k)
childState = cs
if blockSignals:
self.blockTreeChangeSignal()
@ -311,14 +337,14 @@ class Parameter(QtCore.QObject):
for ch in childState:
name = ch['name']
typ = ch['type']
#typ = ch.get('type', None)
#print('child: %s, %s' % (self.name()+'.'+name, typ))
## First, see if there is already a child with this name and type
## First, see if there is already a child with this name
gotChild = False
for i, ch2 in enumerate(self.childs[ptr:]):
#print " ", ch2.name(), ch2.type()
if ch2.name() != name or not ch2.isType(typ):
if ch2.name() != name: # or not ch2.isType(typ):
continue
gotChild = True
#print " found it"
@ -393,15 +419,22 @@ class Parameter(QtCore.QObject):
Note that the value of the parameter can *always* be changed by
calling setValue().
"""
return not self.opts.get('readonly', False)
return not self.readonly()
def setWritable(self, writable=True):
"""Set whether this Parameter should be editable by the user. (This is
exactly the opposite of setReadonly)."""
self.setOpts(readonly=not writable)
def readonly(self):
"""
Return True if this parameter is read-only. (this is the opposite of writable())
"""
return self.opts.get('readonly', False)
def setReadonly(self, readonly=True):
"""Set whether this Parameter's value may be edited by the user."""
"""Set whether this Parameter's value may be edited by the user
(this is the opposite of setWritable())."""
self.setOpts(readonly=readonly)
def setOpts(self, **opts):
@ -453,11 +486,20 @@ class Parameter(QtCore.QObject):
return ParameterItem(self, depth=depth)
def addChild(self, child):
"""Add another parameter to the end of this parameter's child list."""
return self.insertChild(len(self.childs), child)
def addChild(self, child, autoIncrementName=None):
"""
Add another parameter to the end of this parameter's child list.
See insertChild() for a description of the *autoIncrementName*
argument.
"""
return self.insertChild(len(self.childs), child, autoIncrementName=autoIncrementName)
def addChildren(self, children):
"""
Add a list or dict of children to this parameter. This method calls
addChild once for each value in *children*.
"""
## If children was specified as dict, then assume keys are the names.
if isinstance(children, dict):
ch2 = []
@ -473,19 +515,24 @@ class Parameter(QtCore.QObject):
self.addChild(chOpts)
def insertChild(self, pos, child):
def insertChild(self, pos, child, autoIncrementName=None):
"""
Insert a new child at pos.
If pos is a Parameter, then insert at the position of that Parameter.
If child is a dict, then a parameter is constructed using
:func:`Parameter.create <pyqtgraph.parametertree.Parameter.create>`.
By default, the child's 'autoIncrementName' option determines whether
the name will be adjusted to avoid prior name collisions. This
behavior may be overridden by specifying the *autoIncrementName*
argument. This argument was added in version 0.9.9.
"""
if isinstance(child, dict):
child = Parameter.create(**child)
name = child.name()
if name in self.names and child is not self.names[name]:
if child.opts.get('autoIncrementName', False):
if autoIncrementName is True or (autoIncrementName is None and child.opts.get('autoIncrementName', False)):
name = self.incrementName(name)
child.setName(name)
else:
@ -550,6 +597,7 @@ class Parameter(QtCore.QObject):
if parent is None:
raise Exception("Cannot remove; no parent.")
parent.removeChild(self)
self.sigRemoved.emit(self)
def incrementName(self, name):
## return an unused name by adding a number to the name given
@ -590,9 +638,12 @@ class Parameter(QtCore.QObject):
names = (names,)
return self.param(*names).setValue(value)
def param(self, *names):
def child(self, *names):
"""Return a child parameter.
Accepts the name of the child or a tuple (path, to, child)"""
Accepts the name of the child or a tuple (path, to, child)
Added in version 0.9.9. Ealier versions used the 'param' method, which is still
implemented for backward compatibility."""
try:
param = self.names[names[0]]
except KeyError:
@ -603,8 +654,12 @@ class Parameter(QtCore.QObject):
else:
return param
def param(self, *names):
# for backward compatibility.
return self.child(*names)
def __repr__(self):
return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self))
return asUnicode("<%s '%s' at 0x%x>") % (self.__class__.__name__, self.name(), id(self))
def __getattr__(self, attr):
## Leaving this undocumented because I might like to remove it in the future..
@ -692,7 +747,8 @@ class Parameter(QtCore.QObject):
if self.blockTreeChangeEmit == 0:
changes = self.treeStateChanges
self.treeStateChanges = []
self.sigTreeStateChanged.emit(self, changes)
if len(changes) > 0:
self.sigTreeStateChanged.emit(self, changes)
class SignalBlocker(object):

View File

@ -1,4 +1,5 @@
from ..Qt import QtGui, QtCore
from ..python2_3 import asUnicode
import os, weakref, re
class ParameterItem(QtGui.QTreeWidgetItem):
@ -15,8 +16,11 @@ class ParameterItem(QtGui.QTreeWidgetItem):
"""
def __init__(self, param, depth=0):
QtGui.QTreeWidgetItem.__init__(self, [param.name(), ''])
title = param.opts.get('title', None)
if title is None:
title = param.name()
QtGui.QTreeWidgetItem.__init__(self, [title, ''])
self.param = param
self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging)
self.depth = depth
@ -30,7 +34,6 @@ class ParameterItem(QtGui.QTreeWidgetItem):
param.sigOptionsChanged.connect(self.optsChanged)
param.sigParentChanged.connect(self.parentChanged)
opts = param.opts
## Generate context menu for renaming/removing parameter
@ -38,6 +41,8 @@ class ParameterItem(QtGui.QTreeWidgetItem):
self.contextMenu.addSeparator()
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
if opts.get('renamable', False):
if param.opts.get('title', None) is not None:
raise Exception("Cannot make parameter with both title != None and renamable == True.")
flags |= QtCore.Qt.ItemIsEditable
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if opts.get('removable', False):
@ -107,15 +112,15 @@ class ParameterItem(QtGui.QTreeWidgetItem):
self.contextMenu.popup(ev.globalPos())
def columnChangedEvent(self, col):
"""Called when the text in a column has been edited.
"""Called when the text in a column has been edited (or otherwise changed).
By default, we only use changes to column 0 to rename the parameter.
"""
if col == 0:
if col == 0 and (self.param.opts.get('title', None) is None):
if self.ignoreNameColumnChange:
return
try:
newName = self.param.setName(str(self.text(col)))
except:
newName = self.param.setName(asUnicode(self.text(col)))
except Exception:
self.setText(0, self.param.name())
raise
@ -127,8 +132,9 @@ class ParameterItem(QtGui.QTreeWidgetItem):
def nameChanged(self, param, name):
## called when the parameter's name has changed.
self.setText(0, name)
if self.param.opts.get('title', None) is None:
self.setText(0, name)
def limitsChanged(self, param, limits):
"""Called when the parameter's limits have changed"""
pass

View File

@ -0,0 +1,127 @@
from .parameterTypes import GroupParameter
from .. import functions as fn
from .SystemSolver import SystemSolver
class ParameterSystem(GroupParameter):
"""
ParameterSystem is a subclass of GroupParameter that manages a tree of
sub-parameters with a set of interdependencies--changing any one parameter
may affect other parameters in the system.
See parametertree/SystemSolver for more information.
NOTE: This API is experimental and may change substantially across minor
version numbers.
"""
def __init__(self, *args, **kwds):
GroupParameter.__init__(self, *args, **kwds)
self._system = None
self._fixParams = [] # all auto-generated 'fixed' params
sys = kwds.pop('system', None)
if sys is not None:
self.setSystem(sys)
self._ignoreChange = [] # params whose changes should be ignored temporarily
self.sigTreeStateChanged.connect(self.updateSystem)
def setSystem(self, sys):
self._system = sys
# auto-generate defaults to match child parameters
defaults = {}
vals = {}
for param in self:
name = param.name()
constraints = ''
if hasattr(sys, '_' + name):
constraints += 'n'
if not param.readonly():
constraints += 'f'
if 'n' in constraints:
ch = param.addChild(dict(name='fixed', type='bool', value=False))
self._fixParams.append(ch)
param.setReadonly(True)
param.setOpts(expanded=False)
else:
vals[name] = param.value()
ch = param.addChild(dict(name='fixed', type='bool', value=True, readonly=True))
#self._fixParams.append(ch)
defaults[name] = [None, param.type(), None, constraints]
sys.defaultState.update(defaults)
sys.reset()
for name, value in vals.items():
setattr(sys, name, value)
self.updateAllParams()
def updateSystem(self, param, changes):
changes = [ch for ch in changes if ch[0] not in self._ignoreChange]
#resets = [ch[0] for ch in changes if ch[1] == 'setToDefault']
sets = [ch[0] for ch in changes if ch[1] == 'value']
#for param in resets:
#setattr(self._system, param.name(), None)
for param in sets:
#if param in resets:
#continue
#if param in self._fixParams:
#param.parent().setWritable(param.value())
#else:
if param in self._fixParams:
parent = param.parent()
if param.value():
setattr(self._system, parent.name(), parent.value())
else:
setattr(self._system, parent.name(), None)
else:
setattr(self._system, param.name(), param.value())
self.updateAllParams()
def updateAllParams(self):
try:
self.sigTreeStateChanged.disconnect(self.updateSystem)
for name, state in self._system._vars.items():
param = self.child(name)
try:
v = getattr(self._system, name)
if self._system._vars[name][2] is None:
self.updateParamState(self.child(name), 'autoSet')
param.setValue(v)
else:
self.updateParamState(self.child(name), 'fixed')
except RuntimeError:
self.updateParamState(param, 'autoUnset')
finally:
self.sigTreeStateChanged.connect(self.updateSystem)
def updateParamState(self, param, state):
if state == 'autoSet':
bg = fn.mkBrush((200, 255, 200, 255))
bold = False
readonly = True
elif state == 'autoUnset':
bg = fn.mkBrush(None)
bold = False
readonly = False
elif state == 'fixed':
bg = fn.mkBrush('y')
bold = True
readonly = False
param.setReadonly(readonly)
#for item in param.items:
#item.setBackground(0, bg)
#f = item.font(0)
#f.setWeight(f.Bold if bold else f.Normal)
#item.setFont(0, f)

View File

@ -0,0 +1,381 @@
from collections import OrderedDict
import numpy as np
class SystemSolver(object):
"""
This abstract class is used to formalize and manage user interaction with a
complex system of equations (related to "constraint satisfaction problems").
It is often the case that devices must be controlled
through a large number of free variables, and interactions between these
variables make the system difficult to manage and conceptualize as a user
interface. This class does _not_ attempt to numerically solve the system
of equations. Rather, it provides a framework for subdividing the system
into manageable pieces and specifying closed-form solutions to these small
pieces.
For an example, see the simple Camera class below.
Theory of operation: Conceptualize the system as 1) a set of variables
whose values may be either user-specified or automatically generated, and
2) a set of functions that define *how* each variable should be generated.
When a variable is accessed (as an instance attribute), the solver first
checks to see if it already has a value (either user-supplied, or cached
from a previous calculation). If it does not, then the solver calls a
method on itself (the method must be named `_variableName`) that will
either return the calculated value (which usually involves acccessing
other variables in the system), or raise RuntimeError if it is unable to
calculate the value (usually because the user has not provided sufficient
input to fully constrain the system).
Each method that calculates a variable value may include multiple
try/except blocks, so that if one method generates a RuntimeError, it may
fall back on others.
In this way, the system may be solved by recursively searching the tree of
possible relationships between variables. This allows the user flexibility
in deciding which variables are the most important to specify, while
avoiding the apparent combinatorial explosion of calculation pathways
that must be considered by the developer.
Solved values are cached for efficiency, and automatically cleared when
a state change invalidates the cache. The rules for this are simple: any
time a value is set, it invalidates the cache *unless* the previous value
was None (which indicates that no other variable has yet requested that
value). More complex cache management may be defined in subclasses.
Subclasses must define:
1) The *defaultState* class attribute: This is a dict containing a
description of the variables in the system--their default values,
data types, and the ways they can be constrained. The format is::
{ name: [value, type, constraint, allowed_constraints], ...}
* *value* is the default value. May be None if it has not been specified
yet.
* *type* may be float, int, bool, np.ndarray, ...
* *constraint* may be None, single value, or (min, max)
* None indicates that the value is not constrained--it may be
automatically generated if the value is requested.
* *allowed_constraints* is a string composed of (n)one, (f)ixed, and (r)ange.
Note: do not put mutable objects inside defaultState!
2) For each variable that may be automatically determined, a method must
be defined with the name `_variableName`. This method may either return
the
"""
defaultState = OrderedDict()
def __init__(self):
self.__dict__['_vars'] = OrderedDict()
self.__dict__['_currentGets'] = set()
self.reset()
def reset(self):
"""
Reset all variables in the solver to their default state.
"""
self._currentGets.clear()
for k in self.defaultState:
self._vars[k] = self.defaultState[k][:]
def __getattr__(self, name):
if name in self._vars:
return self.get(name)
raise AttributeError(name)
def __setattr__(self, name, value):
"""
Set the value of a state variable.
If None is given for the value, then the constraint will also be set to None.
If a tuple is given for a scalar variable, then the tuple is used as a range constraint instead of a value.
Otherwise, the constraint is set to 'fixed'.
"""
# First check this is a valid attribute
if name in self._vars:
if value is None:
self.set(name, value, None)
elif isinstance(value, tuple) and self._vars[name][1] is not np.ndarray:
self.set(name, None, value)
else:
self.set(name, value, 'fixed')
else:
# also allow setting any other pre-existing attribute
if hasattr(self, name):
object.__setattr__(self, name, value)
else:
raise AttributeError(name)
def get(self, name):
"""
Return the value for parameter *name*.
If the value has not been specified, then attempt to compute it from
other interacting parameters.
If no value can be determined, then raise RuntimeError.
"""
if name in self._currentGets:
raise RuntimeError("Cyclic dependency while calculating '%s'." % name)
self._currentGets.add(name)
try:
v = self._vars[name][0]
if v is None:
cfunc = getattr(self, '_' + name, None)
if cfunc is None:
v = None
else:
v = cfunc()
if v is None:
raise RuntimeError("Parameter '%s' is not specified." % name)
v = self.set(name, v)
finally:
self._currentGets.remove(name)
return v
def set(self, name, value=None, constraint=True):
"""
Set a variable *name* to *value*. The actual set value is returned (in
some cases, the value may be cast into another type).
If *value* is None, then the value is left to be determined in the
future. At any time, the value may be re-assigned arbitrarily unless
a constraint is given.
If *constraint* is True (the default), then supplying a value that
violates a previously specified constraint will raise an exception.
If *constraint* is 'fixed', then the value is set (if provided) and
the variable will not be updated automatically in the future.
If *constraint* is a tuple, then the value is constrained to be within the
given (min, max). Either constraint may be None to disable
it. In some cases, a constraint cannot be satisfied automatically,
and the user will be forced to resolve the constraint manually.
If *constraint* is None, then any constraints are removed for the variable.
"""
var = self._vars[name]
if constraint is None:
if 'n' not in var[3]:
raise TypeError("Empty constraints not allowed for '%s'" % name)
var[2] = constraint
elif constraint == 'fixed':
if 'f' not in var[3]:
raise TypeError("Fixed constraints not allowed for '%s'" % name)
var[2] = constraint
elif isinstance(constraint, tuple):
if 'r' not in var[3]:
raise TypeError("Range constraints not allowed for '%s'" % name)
assert len(constraint) == 2
var[2] = constraint
elif constraint is not True:
raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint)
# type checking / massaging
if var[1] is np.ndarray:
value = np.array(value, dtype=float)
elif var[1] in (int, float, tuple) and value is not None:
value = var[1](value)
# constraint checks
if constraint is True and not self.check_constraint(name, value):
raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2]))
# invalidate other dependent values
if var[0] is not None:
# todo: we can make this more clever..(and might need to)
# we just know that a value of None cannot have dependencies
# (because if anyone else had asked for this value, it wouldn't be
# None anymore)
self.resetUnfixed()
var[0] = value
return value
def check_constraint(self, name, value):
c = self._vars[name][2]
if c is None or value is None:
return True
if isinstance(c, tuple):
return ((c[0] is None or c[0] <= value) and
(c[1] is None or c[1] >= value))
else:
return value == c
def saveState(self):
"""
Return a serializable description of the solver's current state.
"""
state = OrderedDict()
for name, var in self._vars.items():
state[name] = (var[0], var[2])
return state
def restoreState(self, state):
"""
Restore the state of all values and constraints in the solver.
"""
self.reset()
for name, var in state.items():
self.set(name, var[0], var[1])
def resetUnfixed(self):
"""
For any variable that does not have a fixed value, reset
its value to None.
"""
for var in self._vars.values():
if var[2] != 'fixed':
var[0] = None
def solve(self):
for k in self._vars:
getattr(self, k)
def __repr__(self):
state = OrderedDict()
for name, var in self._vars.items():
if var[2] == 'fixed':
state[name] = var[0]
state = ', '.join(["%s=%s" % (n, v) for n,v in state.items()])
return "<%s %s>" % (self.__class__.__name__, state)
if __name__ == '__main__':
class Camera(SystemSolver):
"""
Consider a simple SLR camera. The variables we will consider that
affect the camera's behavior while acquiring a photo are aperture, shutter speed,
ISO, and flash (of course there are many more, but let's keep the example simple).
In rare cases, the user wants to manually specify each of these variables and
no more work needs to be done to take the photo. More often, the user wants to
specify more interesting constraints like depth of field, overall exposure,
or maximum allowed ISO value.
If we add a simple light meter measurement into this system and an 'exposure'
variable that indicates the desired exposure (0 is "perfect", -1 is one stop
darker, etc), then the system of equations governing the camera behavior would
have the following variables:
aperture, shutter, iso, flash, exposure, light meter
The first four variables are the "outputs" of the system (they directly drive
the camera), the last is a constant (the camera itself cannot affect the
reading on the light meter), and 'exposure' specifies a desired relationship
between other variables in the system.
So the question is: how can I formalize a system like this as a user interface?
Typical cameras have a fairly limited approach: provide the user with a list
of modes, each of which defines a particular set of constraints. For example:
manual: user provides aperture, shutter, iso, and flash
aperture priority: user provides aperture and exposure, camera selects
iso, shutter, and flash automatically
shutter priority: user provides shutter and exposure, camera selects
iso, aperture, and flash
program: user specifies exposure, camera selects all other variables
automatically
action: camera selects all variables while attempting to maximize
shutter speed
portrait: camera selects all variables while attempting to minimize
aperture
A more general approach might allow the user to provide more explicit
constraints on each variable (for example: I want a shutter speed of 1/30 or
slower, an ISO no greater than 400, an exposure between -1 and 1, and the
smallest aperture possible given all other constraints) and have the camera
solve the system of equations, with a warning if no solution is found. This
is exactly what we will implement in this example class.
"""
defaultState = OrderedDict([
# Field stop aperture
('aperture', [None, float, None, 'nf']),
# Duration that shutter is held open.
('shutter', [None, float, None, 'nf']),
# ISO (sensitivity) value. 100, 200, 400, 800, 1600..
('iso', [None, int, None, 'nf']),
# Flash is a value indicating the brightness of the flash. A table
# is used to decide on "balanced" settings for each flash level:
# 0: no flash
# 1: s=1/60, a=2.0, iso=100
# 2: s=1/60, a=4.0, iso=100 ..and so on..
('flash', [None, float, None, 'nf']),
# exposure is a value indicating how many stops brighter (+1) or
# darker (-1) the photographer would like the photo to appear from
# the 'balanced' settings indicated by the light meter (see below).
('exposure', [None, float, None, 'f']),
# Let's define this as an external light meter (not affected by
# aperture) with logarithmic output. We arbitrarily choose the
# following settings as "well balanced" for each light meter value:
# -1: s=1/60, a=2.0, iso=100
# 0: s=1/60, a=4.0, iso=100
# 1: s=1/120, a=4.0, iso=100 ..and so on..
# Note that the only allowed constraint mode is (f)ixed, since the
# camera never _computes_ the light meter value, it only reads it.
('lightMeter', [None, float, None, 'f']),
# Indicates the camera's final decision on how it thinks the photo will
# look, given the chosen settings. This value is _only_ determined
# automatically.
('balance', [None, float, None, 'n']),
])
def _aperture(self):
"""
Determine aperture automatically under a variety of conditions.
"""
iso = self.iso
exp = self.exposure
light = self.lightMeter
try:
# shutter-priority mode
sh = self.shutter # this raises RuntimeError if shutter has not
# been specified
ap = 4.0 * (sh / (1./60.)) * (iso / 100.) * (2 ** exp) * (2 ** light)
ap = np.clip(ap, 2.0, 16.0)
except RuntimeError:
# program mode; we can select a suitable shutter
# value at the same time.
sh = (1./60.)
raise
return ap
def _balance(self):
iso = self.iso
light = self.lightMeter
sh = self.shutter
ap = self.aperture
fl = self.flash
bal = (4.0 / ap) * (sh / (1./60.)) * (iso / 100.) * (2 ** light)
return np.log2(bal)
camera = Camera()
camera.iso = 100
camera.exposure = 0
camera.lightMeter = 2
camera.shutter = 1./60.
camera.flash = 0
camera.solve()
print camera.saveState()

View File

@ -1,5 +1,5 @@
from .Parameter import Parameter, registerParameterType
from .ParameterTree import ParameterTree
from .ParameterItem import ParameterItem
from .ParameterSystem import ParameterSystem, SystemSolver
from . import parameterTypes as types

View File

@ -78,6 +78,7 @@ class WidgetParameterItem(ParameterItem):
## no starting value was given; use whatever the widget has
self.widgetValueChanged()
self.updateDefaultBtn()
def makeWidget(self):
"""
@ -191,6 +192,9 @@ class WidgetParameterItem(ParameterItem):
def updateDefaultBtn(self):
## enable/disable default btn
self.defaultBtn.setEnabled(not self.param.valueIsDefault() and self.param.writable())
# hide / show
self.defaultBtn.setVisible(not self.param.readonly())
def updateDisplayLabel(self, value=None):
"""Update the display label to reflect the value of the parameter."""
@ -234,6 +238,8 @@ class WidgetParameterItem(ParameterItem):
self.widget.show()
self.displayLabel.hide()
self.widget.setFocus(QtCore.Qt.OtherFocusReason)
if isinstance(self.widget, SpinBox):
self.widget.selectNumber() # select the numerical portion of the text for quick editing
def hideEditor(self):
self.widget.hide()
@ -277,7 +283,7 @@ class WidgetParameterItem(ParameterItem):
if 'readonly' in opts:
self.updateDefaultBtn()
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
w.setEnabled(not opts['readonly'])
self.widget.setEnabled(not opts['readonly'])
## If widget is a SpinBox, pass options straight through
if isinstance(self.widget, SpinBox):
@ -315,8 +321,8 @@ class SimpleParameter(Parameter):
def colorValue(self):
return fn.mkColor(Parameter.value(self))
def saveColorState(self):
state = Parameter.saveState(self)
def saveColorState(self, *args, **kwds):
state = Parameter.saveState(self, *args, **kwds)
state['value'] = fn.colorTuple(self.value())
return state
@ -539,7 +545,6 @@ class ListParameter(Parameter):
self.forward, self.reverse = self.mapping(limits)
Parameter.setLimits(self, limits)
#print self.name(), self.value(), limits, self.reverse
if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]:
self.setValue(self.reverse[0][0])

View File

@ -61,6 +61,19 @@ def test_interpolateArray():
assert_array_almost_equal(r1, r2)
def test_subArray():
a = np.array([0, 0, 111, 112, 113, 0, 121, 122, 123, 0, 0, 0, 211, 212, 213, 0, 221, 222, 223, 0, 0, 0, 0])
b = pg.subArray(a, offset=2, shape=(2,2,3), stride=(10,4,1))
c = np.array([[[111,112,113], [121,122,123]], [[211,212,213], [221,222,223]]])
assert np.all(b == c)
# operate over first axis; broadcast over the rest
aa = np.vstack([a, a/100.]).T
cc = np.empty(c.shape + (2,))
cc[..., 0] = c
cc[..., 1] = c / 100.
bb = pg.subArray(aa, offset=2, shape=(2,2,3), stride=(10,4,1))
assert np.all(bb == cc)

View File

@ -0,0 +1,50 @@
import gc
from ..Qt import QtCore
class GarbageCollector(object):
'''
Disable automatic garbage collection and instead collect manually
on a timer.
This is done to ensure that garbage collection only happens in the GUI
thread, as otherwise Qt can crash.
Credit: Erik Janssens
Source: http://pydev.blogspot.com/2014/03/should-python-garbage-collector-be.html
'''
def __init__(self, interval=1.0, debug=False):
self.debug = debug
if debug:
gc.set_debug(gc.DEBUG_LEAK)
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.check)
self.threshold = gc.get_threshold()
gc.disable()
self.timer.start(interval * 1000)
def check(self):
#return self.debug_cycles() # uncomment to just debug cycles
l0, l1, l2 = gc.get_count()
if self.debug:
print('gc_check called:', l0, l1, l2)
if l0 > self.threshold[0]:
num = gc.collect(0)
if self.debug:
print('collecting gen 0, found: %d unreachable' % num)
if l1 > self.threshold[1]:
num = gc.collect(1)
if self.debug:
print('collecting gen 1, found: %d unreachable' % num)
if l2 > self.threshold[2]:
num = gc.collect(2)
if self.debug:
print('collecting gen 2, found: %d unreachable' % num)
def debug_cycles(self):
gc.collect()
for obj in gc.garbage:
print (obj, repr(obj), type(obj))

View File

@ -19,8 +19,8 @@ class ColorMapWidget(ptree.ParameterTree):
"""
sigColorMapChanged = QtCore.Signal(object)
def __init__(self):
ptree.ParameterTree.__init__(self, showHeader=False)
def __init__(self, parent=None):
ptree.ParameterTree.__init__(self, parent=parent, showHeader=False)
self.params = ColorMapParameter()
self.setParameters(self.params)
@ -32,6 +32,15 @@ class ColorMapWidget(ptree.ParameterTree):
def mapChanged(self):
self.sigColorMapChanged.emit(self)
def widgetGroupInterface(self):
return (self.sigColorMapChanged, self.saveState, self.restoreState)
def saveState(self):
return self.params.saveState()
def restoreState(self, state):
self.params.restoreState(state)
class ColorMapParameter(ptree.types.GroupParameter):
@ -48,9 +57,11 @@ class ColorMapParameter(ptree.types.GroupParameter):
def addNew(self, name):
mode = self.fields[name].get('mode', 'range')
if mode == 'range':
self.addChild(RangeColorMapItem(name, self.fields[name]))
item = RangeColorMapItem(name, self.fields[name])
elif mode == 'enum':
self.addChild(EnumColorMapItem(name, self.fields[name]))
item = EnumColorMapItem(name, self.fields[name])
self.addChild(item)
return item
def fieldNames(self):
return self.fields.keys()
@ -95,6 +106,9 @@ class ColorMapParameter(ptree.types.GroupParameter):
returned as 0.0-1.0 float values.
============== =================================================================
"""
if isinstance(data, dict):
data = np.array([tuple(data.values())], dtype=[(k, float) for k in data.keys()])
colors = np.zeros((len(data),4))
for item in self.children():
if not item['Enabled']:
@ -126,8 +140,26 @@ class ColorMapParameter(ptree.types.GroupParameter):
return colors
def saveState(self):
items = OrderedDict()
for item in self:
itemState = item.saveState(filter='user')
itemState['field'] = item.fieldName
items[item.name()] = itemState
state = {'fields': self.fields, 'items': items}
return state
def restoreState(self, state):
if 'fields' in state:
self.setFields(state['fields'])
for itemState in state['items']:
item = self.addNew(itemState['field'])
item.restoreState(itemState)
class RangeColorMapItem(ptree.types.SimpleParameter):
mapType = 'range'
def __init__(self, name, opts):
self.fieldName = name
units = opts.get('units', '')
@ -151,8 +183,6 @@ class RangeColorMapItem(ptree.types.SimpleParameter):
def map(self, data):
data = data[self.fieldName]
scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
cmap = self.value()
colors = cmap.map(scaled, mode='float')
@ -162,10 +192,11 @@ class RangeColorMapItem(ptree.types.SimpleParameter):
nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.)
colors[mask] = nanColor
return colors
return colors
class EnumColorMapItem(ptree.types.GroupParameter):
mapType = 'enum'
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])

View File

@ -1,5 +1,6 @@
from ..Qt import QtGui, QtCore
from ..SignalProxy import SignalProxy
import sys
from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode
@ -20,6 +21,10 @@ class ComboBox(QtGui.QComboBox):
self.currentIndexChanged.connect(self.indexChanged)
self._ignoreIndexChange = False
#self.value = default
if 'darwin' in sys.platform: ## because MacOSX can show names that are wider than the comboBox
self.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
#self.setMinimumContentsLength(10)
self._chosenText = None
self._items = OrderedDict()

View File

@ -57,7 +57,7 @@ class DataTreeWidget(QtGui.QTreeWidget):
}
if isinstance(data, dict):
for k in data:
for k in data.keys():
self.buildTree(data[k], node, str(k))
elif isinstance(data, list) or isinstance(data, tuple):
for i in range(len(data)):

View File

@ -47,29 +47,29 @@ class SpinBox(QtGui.QAbstractSpinBox):
"""
============== ========================================================================
**Arguments:**
parent Sets the parent widget for this SpinBox (optional)
value (float/int) initial value
parent Sets the parent widget for this SpinBox (optional). Default is None.
value (float/int) initial value. Default is 0.0.
bounds (min,max) Minimum and maximum values allowed in the SpinBox.
Either may be None to leave the value unbounded.
suffix (str) suffix (units) to display after the numerical value
Either may be None to leave the value unbounded. By default, values are unbounded.
suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str.
siPrefix (bool) If True, then an SI prefix is automatically prepended
to the units and the value is scaled accordingly. For example,
if value=0.003 and suffix='V', then the SpinBox will display
"300 mV" (but a call to SpinBox.value will still return 0.003).
"300 mV" (but a call to SpinBox.value will still return 0.003). Default is False.
step (float) The size of a single step. This is used when clicking the up/
down arrows, when rolling the mouse wheel, or when pressing
keyboard arrows while the widget has keyboard focus. Note that
the interpretation of this value is different when specifying
the 'dec' argument.
the 'dec' argument. Default is 0.01.
dec (bool) If True, then the step value will be adjusted to match
the current size of the variable (for example, a value of 15
might step in increments of 1 whereas a value of 1500 would
step in increments of 100). In this case, the 'step' argument
is interpreted *relative* to the current value. The most common
'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0.
'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False.
minStep (float) When dec=True, this specifies the minimum allowable step size.
int (bool) if True, the value is forced to integer type
decimals (int) Number of decimal values to display
int (bool) if True, the value is forced to integer type. Default is False
decimals (int) Number of decimal values to display. Default is 2.
============== ========================================================================
"""
QtGui.QAbstractSpinBox.__init__(self, parent)
@ -233,6 +233,18 @@ class SpinBox(QtGui.QAbstractSpinBox):
def setDecimals(self, decimals):
self.setOpts(decimals=decimals)
def selectNumber(self):
"""
Select the numerical portion of the text to allow quick editing by the user.
"""
le = self.lineEdit()
text = le.text()
try:
index = text.index(' ')
except ValueError:
return
le.setSelection(0, index)
def value(self):
"""

View File

@ -365,7 +365,7 @@ class TableWidget(QtGui.QTableWidget):
ev.ignore()
def handleItemChanged(self, item):
item.textChanged()
item.itemChanged()
class TableWidgetItem(QtGui.QTableWidgetItem):
@ -425,7 +425,8 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
def _updateText(self):
self._blockValueChange = True
try:
self.setText(self.format())
self._text = self.format()
self.setText(self._text)
finally:
self._blockValueChange = False
@ -433,14 +434,22 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
self.value = value
self._updateText()
def itemChanged(self):
"""Called when the data of this item has changed."""
if self.text() != self._text:
self.textChanged()
def textChanged(self):
"""Called when this item's text has changed for any reason."""
self._text = self.text()
if self._blockValueChange:
# text change was result of value or format change; do not
# propagate.
return
try:
self.value = type(self.value)(self.text())
except ValueError:
self.value = str(self.text())