From 753ac9b4c4e19e417b32b7d82d056e4bc0117b54 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:03:26 -0400 Subject: [PATCH 01/33] Squashed commit of the following: commit ca3fbe2ff9d07162e4cc4488f4280f0b189f86e8 Author: Luke Campagnola 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 --- pyqtgraph/Vector.py | 2 + pyqtgraph/__init__.py | 9 +- pyqtgraph/canvas/Canvas.py | 18 +- pyqtgraph/canvas/CanvasTemplate.ui | 28 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 53 +-- pyqtgraph/colormap.py | 5 +- pyqtgraph/configfile.py | 17 +- pyqtgraph/console/Console.py | 11 + pyqtgraph/console/template.ui | 42 +- pyqtgraph/console/template_pyqt.py | 35 +- pyqtgraph/debug.py | 127 +++++- pyqtgraph/exceptionHandling.py | 54 ++- pyqtgraph/exporters/CSVExporter.py | 32 +- pyqtgraph/exporters/HDF5Exporter.py | 58 +++ pyqtgraph/exporters/Matplotlib.py | 61 ++- pyqtgraph/exporters/SVGExporter.py | 33 +- pyqtgraph/exporters/__init__.py | 2 +- pyqtgraph/flowchart/Flowchart.py | 57 +-- pyqtgraph/flowchart/library/Data.py | 4 +- pyqtgraph/flowchart/library/Filters.py | 70 +++- pyqtgraph/flowchart/library/common.py | 36 ++ pyqtgraph/flowchart/library/functions.py | 2 +- pyqtgraph/functions.py | 92 +++++ pyqtgraph/graphicsItems/AxisItem.py | 16 +- pyqtgraph/graphicsItems/GradientEditorItem.py | 125 +++--- pyqtgraph/graphicsItems/ImageItem.py | 11 +- pyqtgraph/graphicsItems/PlotDataItem.py | 18 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 14 +- pyqtgraph/graphicsItems/ROI.py | 112 ++++- pyqtgraph/graphicsItems/ScaleBar.py | 61 +-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 10 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 33 +- pyqtgraph/imageview/ImageView.py | 51 ++- pyqtgraph/imageview/ImageViewTemplate.ui | 7 +- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 19 +- .../imageview/ImageViewTemplate_pyside.py | 19 +- pyqtgraph/metaarray/MetaArray.py | 72 ++-- pyqtgraph/multiprocess/remoteproxy.py | 223 ++++++---- pyqtgraph/parametertree/Parameter.py | 106 +++-- pyqtgraph/parametertree/ParameterItem.py | 24 +- pyqtgraph/parametertree/ParameterSystem.py | 127 ++++++ pyqtgraph/parametertree/SystemSolver.py | 381 ++++++++++++++++++ pyqtgraph/parametertree/__init__.py | 2 +- pyqtgraph/parametertree/parameterTypes.py | 13 +- pyqtgraph/tests/test_functions.py | 13 + pyqtgraph/util/garbage_collector.py | 50 +++ pyqtgraph/widgets/ColorMapWidget.py | 47 ++- pyqtgraph/widgets/ComboBox.py | 5 + pyqtgraph/widgets/DataTreeWidget.py | 2 +- pyqtgraph/widgets/SpinBox.py | 30 +- pyqtgraph/widgets/TableWidget.py | 13 +- 51 files changed, 1912 insertions(+), 540 deletions(-) create mode 100644 pyqtgraph/exporters/HDF5Exporter.py create mode 100644 pyqtgraph/parametertree/ParameterSystem.py create mode 100644 pyqtgraph/parametertree/SystemSolver.py create mode 100644 pyqtgraph/util/garbage_collector.py diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index b18b3091e2..f2898e80b0 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -81,5 +81,7 @@ def angle(self, a): # ang *= -1. return ang * 180. / np.pi + def __abs__(self): + return Vector(abs(self.x()), abs(self.y()), abs(self.z())) \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 01e84c4902..f8983455d7 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -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) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index d07b3428d6..4de891f795 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -67,8 +67,8 @@ def __init__(self, parent=None, allowTransforms=True, hideCtrl=False, name=None) 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 @@ def __init__(self, parent=None, allowTransforms=True, hideCtrl=False, name=None) 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 @@ def itemListContextMenuEvent(self, ev): 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() diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index 218cf48d4d..9bea8f8921 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -28,21 +28,7 @@ - - - - Store SVG - - - - - - - Store PNG - - - - + @@ -55,7 +41,7 @@ - + 0 @@ -75,7 +61,7 @@ - + @@ -93,28 +79,28 @@ - + 0 - + Reset Transforms - + Mirror Selection - + MirrorXY diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index c809cb1dfd..557354e0a8 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -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 @@ 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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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 diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 446044e17d..c003370829 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -244,4 +244,7 @@ def isMapTrivial(self): 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) diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index f709c78660..c095bba305 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -14,6 +14,10 @@ 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. diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 6d77c4cfcd..896de92467 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -341,6 +341,17 @@ def checkException(self, excType, exc, tb): 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 diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 6e5c5be399..1a672c5e41 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -6,7 +6,7 @@ 0 0 - 710 + 694 497 @@ -89,6 +89,16 @@ 0 + + + + false + + + Clear Exception + + + @@ -109,7 +119,7 @@ - + Only Uncaught Exceptions @@ -119,14 +129,14 @@ - + true - + Run commands in selected stack frame @@ -136,24 +146,14 @@ - + Exception Info - - - - false - - - Clear Exception - - - - + Qt::Horizontal @@ -166,6 +166,16 @@ + + + + Filter (regex): + + + + + + diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index e0852c93a7..354fb1d65b 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -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 @@ def _translate(context, text, disambig): 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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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 @@ def retranslateUi(self, Form): 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 diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0deae0e0e3..57c71bc8cb 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -32,6 +32,57 @@ def w(*args, **kargs): 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 w(*args, **kwds): 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 @@ def function(...): _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 @@ def finish(self): 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 @@ def __new__(cls, msg=None, disabled='env', delayed=True): 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 @@ def __new__(cls, msg=None, disabled='env', delayed=True): 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 @@ def __del__(self): 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 @@ def flush(self): 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 diff --git a/pyqtgraph/exceptionHandling.py b/pyqtgraph/exceptionHandling.py index daa821b779..3182b7ebd3 100644 --- a/pyqtgraph/exceptionHandling.py +++ b/pyqtgraph/exceptionHandling.py @@ -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()) + 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 - ## 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: diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index 6ed4cf07e7..b87f0182e8 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -14,6 +14,7 @@ def __init__(self, item): 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 @@ def export(self, fileName=None): 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 @@ def export(self, fileName=None): 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() diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py new file mode 100644 index 0000000000..cc8b5733e7 --- /dev/null +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -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() diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 57c4cfdb66..8cec1cef5c 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -4,7 +4,29 @@ 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 @@ def __init__(self, item): 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 @@ def export(self, fileName=None): 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") diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index e46c998119..a91466c8a9 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -102,14 +102,12 @@ def export(self, fileName=None, toBytes=False, copy=False): pyqtgraph SVG export Generated with Qt and pyqtgraph - - """ 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\n" + defsXml = "\n" + for d in defs: + defsXml += d.toprettyxml(indent=' ') + defsXml += "\n" + return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n\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 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 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. diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py index 8be1c3b6f1..62ab1331c0 100644 --- a/pyqtgraph/exporters/__init__.py +++ b/pyqtgraph/exporters/__init__.py @@ -4,7 +4,7 @@ from .Matplotlib import * from .CSVExporter import * from .PrintExporter import * - +from .HDF5Exporter import * def listExporters(): return Exporter.Exporters[:] diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 48357b30a9..878f86ae2e 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -20,41 +20,12 @@ 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 @@ def process(self, **args): ## 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 @@ def process(self, **args): 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 @@ def process(self, **args): #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 @@ def processOrder(self): #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 @@ def nodeOutputChanged(self, startNode): 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 @@ def loadFile(self, fileName=None, startDir=None): 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 @@ def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc 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 @@ def loadClicked(self): #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 @@ def saveAsClicked(self): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = str(fileName) + self.currentFileName = unicode(fileName) if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 532f6c5bb3..5236de8d03 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -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) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index b72fbca519..88a2f6c553 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -6,6 +6,8 @@ from .common import * import numpy as np +from ... import PolyLineROI +from ... import Point from ... import metaarray as metaarray @@ -201,6 +203,72 @@ def processData(self, data): 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 @@ def processData(self, data): return ma - \ No newline at end of file + diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 548dc44061..425fe86c9f 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -131,6 +131,42 @@ def showRow(self, name): 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): diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py index 027e1386ee..338d25c416 100644 --- a/pyqtgraph/flowchart/library/functions.py +++ b/pyqtgraph/flowchart/library/functions.py @@ -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 diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 77643c99a6..6ae2f65b00 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -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 diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index ededed5600..e5b9e3f5ae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -918,13 +918,17 @@ def generateDrawSpecs(self, p): 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. diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index e16370f566..a151798a5c 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -3,6 +3,7 @@ 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 @@ def setTickValue(self, tick, val): pos.setX(x) tick.setPos(pos) self.ticks[tick] = val + self.updateGradient() def tickValue(self, tick): ## public @@ -537,23 +539,22 @@ def currentColorAccepted(self): 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 @@ def addTick(self, x, color=None, movable=True, finish=True): def removeTick(self, tick, finish=True): TickSliderItem.removeTick(self, tick) if finish: + self.updateGradient() self.sigGradientChangeFinished.emit(self) @@ -867,44 +869,59 @@ def hoverEvent(self, ev): 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 - + +class TickMenu(QtGui.QMenu): + + def __init__(self, tick, sliderItem): + QtGui.QMenu.__init__(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) - #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() + 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 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 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 mouseReleaseEvent(self, ev): - ##print self, "release", ev.scenePos() - #if not self.movedSincePress: - #self.view().tickClicked(self, ev) - - ##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) + #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) + diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index f5c2d2487e..5c39627c31 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -177,6 +177,12 @@ def setRect(self, rect): 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 @@ def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): 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) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index befc578318..6148989d74 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -168,6 +168,7 @@ def __init__(self, *args, **kargs): 'downsample': 1, 'autoDownsample': False, 'downsampleMethod': 'peak', + 'autoDownsampleFactor': 5., # draw ~5 samples per pixel 'clipToView': False, 'data': None, @@ -380,14 +381,23 @@ def setData(self, *args, **kargs): 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 @@ def getData(self): 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']: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 8292875c50..f8959e22d5 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -469,7 +469,8 @@ def addAvgCurve(self, curve): ### 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 @@ def showButtons(self): 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: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index f3ebd99267..7707466ade 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -25,7 +25,7 @@ __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', ] @@ -862,8 +862,10 @@ def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish= 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 @@ def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish= 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 @@ def boundingRect(self): 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 @@ def paint(self, p, *args): 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)) + + diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py index 662586785e..8ba546f7b4 100644 --- a/pyqtgraph/graphicsItems/ScaleBar.py +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -5,6 +5,7 @@ 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 @@ def __init__(self, size, width=5, brush=None, pen=None, suffix='m'): 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 @@ def updateBar(self): 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 - diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index bdf89c452d..e39b535a45 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -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): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index d66f32ad83..542bbc1a14 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -760,7 +760,8 @@ def translateBy(self, t=None, x=None, y=None): 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 @@ def updateAutoRange(self): 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 @@ def invertY(self, b=True): 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 @@ def updateViewRange(self, forceX=False, forceY=False): 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 @@ def updateViewRange(self, forceX=False, forceY=False): 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() diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c9f421b415..65252cfe4b 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,7 +12,7 @@ - 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 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar self.ui.histogram.setImageItem(self.imageItem) + self.menu = None + self.ui.normGroup.hide() self.roi = PlotROI(10) @@ -176,7 +178,8 @@ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *ar 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 @@ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, 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 @@ def getRoiPlot(self): 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()) + diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui index 9a3dab0369..927bda30aa 100644 --- a/pyqtgraph/imageview/ImageViewTemplate.ui +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -53,7 +53,7 @@ - + 0 @@ -61,10 +61,7 @@ - Norm - - - true + Menu diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index 7815631757..e728b26542 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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)) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index 2f8b570bdb..6d6c96322a 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -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 @@ def setupUi(self, Form): 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 @@ def setupUi(self, Form): 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)) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index d24a7d0538..9c3f5b8a19 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -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 @@ def __init__(self, data=None, info=None, dtype=None, file=None, copy=False, **kw 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 @@ def readFile(self, filename, **kwargs): """ ## 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 @@ def _readMeta(fd): #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 @@ def _readData1(self, fd, meta, mmap=False): 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 @@ def _readData2(self, fd, meta, mmap=False, subset=None): 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 @@ def writeMeta(self, fileName): 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) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 4e7b7a1cea..4f484b7444 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -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 @@ def __init__(self, connection, name, pid, debug=False): ## 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 @@ def __init__(self, connection, name, pid, debug=False): '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 @@ def debugMsg(self, msg): 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 @@ def handleRequest(self): 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 @@ def handleRequest(self): 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 @@ def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, retu traceback ============= ===================================================================== """ - #if len(kwds) > 0: - #print "Warning: send() ignored args:", kwds - - 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 self.exited: + self.debugMsg(' send: exited already; raise ClosedError.') + raise ClosedError() - if returnType is not None: - opts['returnType'] = returnType + with self.sendLock: + #if len(kwds) > 0: + #print "Warning: send() ignored args:", kwds + + if opts is None: + opts = {} - #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) + 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 + + #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 @@ def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, retu 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 @@ def callObj(self, obj, args, kwds, **opts): 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 @@ def callObj(self, obj, args, kwds, **opts): 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 diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 1c75c33316..5f37ccdc08 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -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 @@ def __init__(self, **opts): 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 @@ def __init__(self, **opts): '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 @@ def getValues(self): 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() - """ - 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) + 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. + """ + 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 @@ def restoreState(self, state, recursive=True, addChildren=True, removeChildren=T ## 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 @@ def restoreState(self, state, recursive=True, addChildren=True, removeChildren=T 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 @@ def writable(self): 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 @@ def makeTreeItem(self, depth): 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 @@ def addChildren(self, children): 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 `. + + 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 @@ def remove(self): 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 @@ def __setitem__(self, names, value): 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 @@ def param(self, *names): 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 @@ def emitTreeChanges(self): 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): diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 5a90becf00..c149c4110e 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -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 @@ def __init__(self, param, depth=0): param.sigOptionsChanged.connect(self.optsChanged) param.sigParentChanged.connect(self.parentChanged) - opts = param.opts ## Generate context menu for renaming/removing parameter @@ -38,6 +41,8 @@ def __init__(self, param, depth=0): 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 @@ def contextMenuEvent(self, ev): 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 @@ def columnChangedEvent(self, col): 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 diff --git a/pyqtgraph/parametertree/ParameterSystem.py b/pyqtgraph/parametertree/ParameterSystem.py new file mode 100644 index 0000000000..33bb2de8e9 --- /dev/null +++ b/pyqtgraph/parametertree/ParameterSystem.py @@ -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) + + + + diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py new file mode 100644 index 0000000000..367210f245 --- /dev/null +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -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() + \ No newline at end of file diff --git a/pyqtgraph/parametertree/__init__.py b/pyqtgraph/parametertree/__init__.py index acdb7a37c1..722410d5c0 100644 --- a/pyqtgraph/parametertree/__init__.py +++ b/pyqtgraph/parametertree/__init__.py @@ -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 \ No newline at end of file diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8aba4bcaf8..7b1c5ee610 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -78,6 +78,7 @@ def __init__(self, param, depth): ## no starting value was given; use whatever the widget has self.widgetValueChanged() + self.updateDefaultBtn() def makeWidget(self): """ @@ -191,6 +192,9 @@ def valueChanged(self, param, val, force=False): 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 @@ def showEditor(self): 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 @@ def optsChanged(self, param, opts): 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 @@ def __init__(self, *args, **kargs): 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 @@ def setLimits(self, limits): 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]) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 47fa266d50..f622dd8736 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -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) diff --git a/pyqtgraph/util/garbage_collector.py b/pyqtgraph/util/garbage_collector.py new file mode 100644 index 0000000000..979e66c50e --- /dev/null +++ b/pyqtgraph/util/garbage_collector.py @@ -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)) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 8cd72e153f..f6e289608b 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -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 @@ def __init__(self): 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 @@ def mapChanged(self): 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 @@ def map(self, data, mode='byte'): 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 @@ def map(self, data, mode='byte'): 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 @@ def __init__(self, name, opts): 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 @@ def map(self, data): 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', []) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index f9983c9782..5cf6f9183f 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -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 @@ def __init__(self, parent=None, items=None, default=None): 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() diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index b99121bfe3..29e603191a 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -57,7 +57,7 @@ def buildTree(self, data, parent, name='', hideRoot=False): } 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)): diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 422522de71..23516827e6 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -47,29 +47,29 @@ def __init__(self, parent=None, value=0.0, **kwargs): """ ============== ======================================================================== **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 @@ def setSingleStep(self, step): 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): """ diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 140605463a..69085a2089 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -365,7 +365,7 @@ def keyPressEvent(self, ev): ev.ignore() def handleItemChanged(self, item): - item.textChanged() + item.itemChanged() class TableWidgetItem(QtGui.QTableWidgetItem): @@ -425,7 +425,8 @@ def setFormat(self, fmt): 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 @@ def setValue(self, value): 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()) From 1df5103d94a1f04c534a829a62df2deffedcbfa8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:11:34 -0400 Subject: [PATCH 02/33] Fixes following acq4 merge --- pyqtgraph/exporters/tests/test_csv.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 6 ++++-- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 +- pyqtgraph/parametertree/SystemSolver.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_csv.py b/pyqtgraph/exporters/tests/test_csv.py index 70c69c72fc..a98372ec4f 100644 --- a/pyqtgraph/exporters/tests/test_csv.py +++ b/pyqtgraph/exporters/tests/test_csv.py @@ -29,7 +29,7 @@ def test_CSVExporter(): r = csv.reader(open('test.csv', 'r')) lines = [line for line in r] header = lines.pop(0) - assert header == ['myPlot_x', 'myPlot_y', 'x', 'y', 'x', 'y'] + assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002'] i = 0 for vals in lines: diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 542bbc1a14..ceca62c810 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -427,11 +427,11 @@ def resizeEvent(self, ev): self.linkedYChanged() self.updateAutoRange() self.updateViewRange() + self._matrixNeedsUpdate = True self.sigStateChanged.emit(self) self.background.setRect(self.rect()) self.sigResized.emit(self) - def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy @@ -909,7 +909,7 @@ def updateAutoRange(self): 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)) + #print("Warning: %s is invalid: %s" % (k, str(r)) self.setRange(**args) finally: @@ -1135,6 +1135,8 @@ def childTransform(self): Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) """ + if self._matrixNeedsUpdate: + self.updateMatrix() m = self.childGroup.transform() #m1 = QtGui.QTransform() #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 7cb366c229..f1063e7f11 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -20,11 +20,11 @@ def test_ViewBox(): win.show() vb = win.addViewBox() + # set range before viewbox is shown vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0) # required to make mapFromView work properly. qtest.qWaitForWindowShown(win) - vb.update() g = pg.GridItem() vb.addItem(g) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index 367210f245..0a889dfaf7 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -377,5 +377,5 @@ def _balance(self): camera.flash = 0 camera.solve() - print camera.saveState() + print(camera.saveState()) \ No newline at end of file From 42eae475b9e95cefb3c05a18bd03435c33443570 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:12:31 -0400 Subject: [PATCH 03/33] Disable image downsampling when n=1 --- pyqtgraph/functions.py | 2 ++ pyqtgraph/graphicsItems/ImageItem.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6ae2f65b00..897a123db8 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1222,6 +1222,8 @@ def downsample(data, n, axis=0, xvals='subsample'): data = downsample(data, n[i], axis[i]) return data + if n <= 1: + return data nPts = int(data.shape[axis] / n) s = list(data.shape) s[axis] = nPts diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 5c39627c31..5b0414336b 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -9,6 +9,8 @@ from ..Point import Point __all__ = ['ImageItem'] + + class ImageItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` From 706fe92fdbed99fa25760b0b381bdd4bcf8de180 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Aug 2014 09:13:32 -0400 Subject: [PATCH 04/33] Remove old PIL-fix files, replace with equivalent runtime-patching code. --- pyqtgraph/PIL_Fix/Image.py-1.6 | 2099 ------------------------------- pyqtgraph/PIL_Fix/Image.py-1.7 | 2129 -------------------------------- pyqtgraph/PIL_Fix/README | 11 - pyqtgraph/util/pil_fix.py | 64 + 4 files changed, 64 insertions(+), 4239 deletions(-) delete mode 100644 pyqtgraph/PIL_Fix/Image.py-1.6 delete mode 100644 pyqtgraph/PIL_Fix/Image.py-1.7 delete mode 100644 pyqtgraph/PIL_Fix/README create mode 100644 pyqtgraph/util/pil_fix.py diff --git a/pyqtgraph/PIL_Fix/Image.py-1.6 b/pyqtgraph/PIL_Fix/Image.py-1.6 deleted file mode 100644 index 2b3730599d..0000000000 --- a/pyqtgraph/PIL_Fix/Image.py-1.6 +++ /dev/null @@ -1,2099 +0,0 @@ -# -# The Python Imaging Library. -# $Id: Image.py 2933 2006-12-03 12:08:22Z fredrik $ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# -# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2006 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -VERSION = "1.1.6" - -try: - import warnings -except ImportError: - warnings = None - -class _imaging_not_installed: - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass - -try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. - import _imaging - core = _imaging - del _imaging -except ImportError, v: - core = _imaging_not_installed() - if str(v)[:20] == "Module use of python" and warnings: - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version " - "of Python; most PIL functions will be disabled", - RuntimeWarning - ) - -import ImageMode -import ImagePalette - -import os, string, sys - -# type stuff -from types import IntType, StringType, TupleType - -try: - UnicodeStringType = type(unicode("")) - ## - # (Internal) Checks if an object is a string. If the current - # Python version supports Unicode, this checks for both 8-bit - # and Unicode strings. - def isStringType(t): - return isinstance(t, StringType) or isinstance(t, UnicodeStringType) -except NameError: - def isStringType(t): - return isinstance(t, StringType) - -## -# (Internal) Checks if an object is a tuple. - -def isTupleType(t): - return isinstance(t, TupleType) - -## -# (Internal) Checks if an object is an image object. - -def isImageType(t): - return hasattr(t, "im") - -## -# (Internal) Checks if an object is a string, and that it points to a -# directory. - -def isDirectory(f): - return isStringType(f) and os.path.isdir(f) - -from operator import isNumberType, isSequenceType - -# -# Debug level - -DEBUG = 0 - -# -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 - -# transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 - -# dithers -NONE = 0 -NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default - -# palettes/quantizers -WEB = 0 -ADAPTIVE = 1 - -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 - -# -------------------------------------------------------------------- -# Registries - -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -EXTENSION = {} - -# -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - - # Experimental modes include I;16, I;16B, RGBa, BGR;15, - # and BGR;24. Use these modes only if you know exactly - # what you're doing... - -} - -if sys.byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), - "L": ('|u1', None), - "I": ('%si4' % _ENDIAN, None), # FIXME: is this correct? - "I;16": ('%su2' % _ENDIAN, None), # FIXME: is this correct? - "F": ('%sf4' % _ENDIAN, None), # FIXME: is this correct? - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), -} - -def _conv_type_shape(im): - shape = im.size[::-1] - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return shape, typ - else: - return shape+(extra,), typ - - -MODES = _MODEINFO.keys() -MODES.sort() - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16B") - -## -# Gets the "base" mode for given mode. This function returns "L" for -# images that contain grayscale data, and "RGB" for images that -# contain color data. -# -# @param mode Input mode. -# @return "L" or "RGB". -# @exception KeyError If the input mode was not a standard mode. - -def getmodebase(mode): - return ImageMode.getmode(mode).basemode - -## -# Gets the storage type mode. Given a mode, this function returns a -# single-layer mode suitable for storing individual bands. -# -# @param mode Input mode. -# @return "L", "I", or "F". -# @exception KeyError If the input mode was not a standard mode. - -def getmodetype(mode): - return ImageMode.getmode(mode).basetype - -## -# Gets a list of individual band names. Given a mode, this function -# returns a tuple containing the names of individual bands (use -# {@link #getmodetype} to get the mode used to store each individual -# band. -# -# @param mode Input mode. -# @return A tuple containing band names. The length of the tuple -# gives the number of bands in an image of the given mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebandnames(mode): - return ImageMode.getmode(mode).bands - -## -# Gets the number of individual bands for this mode. -# -# @param mode Input mode. -# @return The number of bands in this mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebands(mode): - return len(ImageMode.getmode(mode).bands) - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -## -# Explicitly loads standard file format drivers. - -def preinit(): - "Load standard file format drivers." - - global _initialized - if _initialized >= 1: - return - - try: - import BmpImagePlugin - except ImportError: - pass - try: - import GifImagePlugin - except ImportError: - pass - try: - import JpegImagePlugin - except ImportError: - pass - try: - import PpmImagePlugin - except ImportError: - pass - try: - import PngImagePlugin - except ImportError: - pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass - - _initialized = 1 - -## -# Explicitly initializes the Python Imaging Library. This function -# loads all available file format drivers. - -def init(): - "Load all file format drivers." - - global _initialized - if _initialized >= 2: - return - - visited = {} - - directories = sys.path - - try: - directories = directories + [os.path.dirname(__file__)] - except NameError: - pass - - # only check directories (including current, if present in the path) - for directory in filter(isDirectory, directories): - fullpath = os.path.abspath(directory) - if visited.has_key(fullpath): - continue - for file in os.listdir(directory): - if file[-14:] == "ImagePlugin.py": - f, e = os.path.splitext(file) - try: - sys.path.insert(0, directory) - try: - __import__(f, globals(), locals(), []) - finally: - del sys.path[0] - except ImportError: - if DEBUG: - print "Image: failed to import", - print f, ":", sys.exc_value - visited[fullpath] = None - - if OPEN or SAVE: - _initialized = 2 - - -# -------------------------------------------------------------------- -# Codec factories (used by tostring/fromstring and ImageFile.load) - -def _getdecoder(mode, decoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get decoder - decoder = getattr(core, decoder_name + "_decoder") - # print decoder, (mode,) + args + extra - return apply(decoder, (mode,) + args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) - -def _getencoder(mode, encoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get encoder - encoder = getattr(core, encoder_name + "_encoder") - # print encoder, (mode,) + args + extra - return apply(encoder, (mode,) + args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - -class _E: - def __init__(self, data): self.data = data - def __coerce__(self, other): return self, _E(other) - def __add__(self, other): return _E((self.data, "__add__", other.data)) - def __mul__(self, other): return _E((self.data, "__mul__", other.data)) - -def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isNumberType(c)): - return c, 0.0 - if (a is stub and b == "__add__" and isNumberType(c)): - return 1.0, c - except TypeError: pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isNumberType(c) and - d == "__add__" and isNumberType(e)): - return c, e - except TypeError: pass - raise ValueError("illegal expression") - - -# -------------------------------------------------------------------- -# Implementation wrapper - -## -# This class represents an image object. To create Image objects, use -# the appropriate factory functions. There's hardly ever any reason -# to call the Image constructor directly. -# -# @see #open -# @see #new -# @see #fromstring - -class Image: - - format = None - format_description = None - - def __init__(self): - self.im = None - self.mode = "" - self.size = (0, 0) - self.palette = None - self.info = {} - self.category = NORMAL - self.readonly = 0 - - def _new(self, im): - new = Image() - new.im = im - new.mode = im.mode - new.size = im.size - new.palette = self.palette - if im.mode == "P": - new.palette = ImagePalette.ImagePalette() - try: - new.info = self.info.copy() - except AttributeError: - # fallback (pre-1.5.2) - new.info = {} - for k, v in self.info: - new.info[k] = v - return new - - _makeself = _new # compatibility - - def _copy(self): - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _dump(self, file=None, format=None): - import tempfile - if not file: - file = tempfile.mktemp() - self.load() - if not format or format == "PPM": - self.im.save_ppm(file) - else: - file = file + "." + format - self.save(file, format) - return file - - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['data'] = self.tostring() - return new - raise AttributeError(name) - - ## - # Returns a string containing pixel data. - # - # @param encoder_name What encoder to use. The default is to - # use the standard "raw" encoder. - # @param *args Extra arguments to the encoder. - # @return An 8-bit string. - - def tostring(self, encoder_name="raw", *args): - "Return image as a binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - data = [] - while 1: - l, s, d = e.encode(bufsize) - data.append(d) - if s: - break - if s < 0: - raise RuntimeError("encoder error %d in tostring" % s) - - return string.join(data, "") - - ## - # Returns the image converted to an X11 bitmap. This method - # only works for mode "1" images. - # - # @param name The name prefix to use for the bitmap variables. - # @return A string containing an X11 bitmap. - # @exception ValueError If the mode is not "1" - - def tobitmap(self, name="image"): - "Return image as an XBM bitmap" - - self.load() - if self.mode != "1": - raise ValueError("not a bitmap") - data = self.tostring("xbm") - return string.join(["#define %s_width %d\n" % (name, self.size[0]), - "#define %s_height %d\n"% (name, self.size[1]), - "static char %s_bits[] = {\n" % name, data, "};"], "") - - ## - # Loads this image with pixel data from a string. - #

- # This method is similar to the {@link #fromstring} function, but - # loads data into this image instead of creating a new image - # object. - - def fromstring(self, data, decoder_name="raw", *args): - "Load data to image from binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - raise ValueError("not enough image data") - if s[1] != 0: - raise ValueError("cannot decode image data") - - ## - # Allocates storage for the image and loads the pixel data. In - # normal cases, you don't need to call this method, since the - # Image class automatically loads an opened image when it is - # accessed for the first time. - # - # @return An image access object. - - def load(self): - "Explicitly load pixel data." - if self.im and self.palette and self.palette.dirty: - # realize palette - apply(self.im.putpalette, self.palette.getdata()) - self.palette.dirty = 0 - self.palette.mode = "RGB" - self.palette.rawmode = None - if self.info.has_key("transparency"): - self.im.putpalettealpha(self.info["transparency"], 0) - self.palette.mode = "RGBA" - if self.im: - return self.im.pixel_access(self.readonly) - - ## - # Verifies the contents of a file. For data read from a file, this - # method attempts to determine if the file is broken, without - # actually decoding the image data. If this method finds any - # problems, it raises suitable exceptions. If you need to load - # the image after using this method, you must reopen the image - # file. - - def verify(self): - "Verify file contents." - pass - - - ## - # Returns a converted copy of this image. For the "P" mode, this - # method translates pixels through the palette. If mode is - # omitted, a mode is chosen so that all information in the image - # and the palette can be represented without a palette. - #

- # The current version supports all possible conversions between - # "L", "RGB" and "CMYK." - #

- # When translating a colour image to black and white (mode "L"), - # the library uses the ITU-R 601-2 luma transform: - #

- # L = R * 299/1000 + G * 587/1000 + B * 114/1000 - #

- # When translating a greyscale image into a bilevel image (mode - # "1"), all non-zero values are set to 255 (white). To use other - # thresholds, use the {@link #Image.point} method. - # - # @def convert(mode, matrix=None) - # @param mode The requested mode. - # @param matrix An optional conversion matrix. If given, this - # should be 4- or 16-tuple containing floating point values. - # @return An Image object. - - def convert(self, mode=None, data=None, dither=None, - palette=WEB, colors=256): - "Convert to other pixel format" - - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - - self.load() - - if data: - # matrix conversion - if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") - im = self.im.convert_matrix(mode, data) - return self._new(im) - - if mode == "P" and palette == ADAPTIVE: - im = self.im.quantize(colors) - return self._new(im) - - # colourspace conversion - if dither is None: - dither = FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) - im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") - - return self._new(im) - - def quantize(self, colors=256, method=0, kmeans=0, palette=None): - - # methods: - # 0 = median cut - # 1 = maximum coverage - - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. - - self.load() - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - raise ValueError("bad mode for palette image") - if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._makeself(im) - - im = self.im.quantize(colors, method, kmeans) - return self._new(im) - - ## - # Copies this image. Use this method if you wish to paste things - # into an image, but still retain the original. - # - # @return An Image object. - - def copy(self): - "Copy raster data" - - self.load() - im = self.im.copy() - return self._new(im) - - ## - # Returns a rectangular region from this image. The box is a - # 4-tuple defining the left, upper, right, and lower pixel - # coordinate. - #

- # This is a lazy operation. Changes to the source image may or - # may not be reflected in the cropped image. To break the - # connection, call the {@link #Image.load} method on the cropped - # copy. - # - # @param The crop rectangle, as a (left, upper, right, lower)-tuple. - # @return An Image object. - - def crop(self, box=None): - "Crop region from image" - - self.load() - if box is None: - return self.copy() - - # lazy operation - return _ImageCrop(self, box) - - ## - # Configures the image file loader so it returns a version of the - # image that as closely as possible matches the given mode and - # size. For example, you can use this method to convert a colour - # JPEG to greyscale while loading it, or to extract a 128x192 - # version from a PCD file. - #

- # Note that this method modifies the Image object in place. If - # the image has already been loaded, this method has no effect. - # - # @param mode The requested mode. - # @param size The requested size. - - def draft(self, mode, size): - "Configure image decoder" - - pass - - def _expand(self, xmargin, ymargin=None): - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) - - ## - # Filters this image using the given filter. For a list of - # available filters, see the ImageFilter module. - # - # @param filter Filter kernel. - # @return An Image object. - # @see ImageFilter - - def filter(self, filter): - "Apply environment filter to image" - - self.load() - - from ImageFilter import Filter - if not isinstance(filter, Filter): - filter = filter() - - if self.im.bands == 1: - return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) - return merge(self.mode, ims) - - ## - # Returns a tuple containing the name of each band in this image. - # For example, getbands on an RGB image returns ("R", "G", "B"). - # - # @return A tuple containing band names. - - def getbands(self): - "Get band names" - - return ImageMode.getmode(self.mode).bands - - ## - # Calculates the bounding box of the non-zero regions in the - # image. - # - # @return The bounding box is returned as a 4-tuple defining the - # left, upper, right, and lower pixel coordinate. If the image - # is completely empty, this method returns None. - - def getbbox(self): - "Get bounding box of actual data (non-zero pixels) in image" - - self.load() - return self.im.getbbox() - - ## - # Returns a list of colors used in this image. - # - # @param maxcolors Maximum number of colors. If this number is - # exceeded, this method returns None. The default limit is - # 256 colors. - # @return An unsorted list of (count, pixel) values. - - def getcolors(self, maxcolors=256): - "Get colors from image, up to given limit" - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - ## - # Returns the contents of this image as a sequence object - # containing pixel values. The sequence object is flattened, so - # that values for line one follow directly after the values of - # line zero, and so on. - #

- # Note that the sequence object returned by this method is an - # internal PIL data type, which only supports certain sequence - # operations. To convert it to an ordinary sequence (e.g. for - # printing), use list(im.getdata()). - # - # @param band What band to return. The default is to return - # all bands. To return a single band, pass in the index - # value (e.g. 0 to get the "R" band from an "RGB" image). - # @return A sequence-like object. - - def getdata(self, band = None): - "Get image data as sequence object." - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - ## - # Gets the the minimum and maximum pixel values for each band in - # the image. - # - # @return For a single-band image, a 2-tuple containing the - # minimum and maximum pixel value. For a multi-band image, - # a tuple containing one 2-tuple for each band. - - def getextrema(self): - "Get min/max value" - - self.load() - if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) - return self.im.getextrema() - - ## - # Returns a PyCObject that points to the internal image memory. - # - # @return A PyCObject object. - - def getim(self): - "Get PyCObject pointer to internal image memory" - - self.load() - return self.im.ptr - - - ## - # Returns the image palette as a list. - # - # @return A list of color values [r, g, b, ...], or None if the - # image has no palette. - - def getpalette(self): - "Get palette contents." - - self.load() - try: - return map(ord, self.im.getpalette()) - except ValueError: - return None # no palette - - - ## - # Returns the pixel value at a given position. - # - # @param xy The coordinate, given as (x, y). - # @return The pixel value. If the image is a multi-layer image, - # this method returns a tuple. - - def getpixel(self, xy): - "Get pixel value" - - self.load() - return self.im.getpixel(xy) - - ## - # Returns the horizontal and vertical projection. - # - # @return Two sequences, indicating where there are non-zero - # pixels along the X-axis and the Y-axis, respectively. - - def getprojection(self): - "Get projection to x and y axes" - - self.load() - x, y = self.im.getprojection() - return map(ord, x), map(ord, y) - - ## - # Returns a histogram for the image. The histogram is returned as - # a list of pixel counts, one for each pixel value in the source - # image. If the image has more than one band, the histograms for - # all bands are concatenated (for example, the histogram for an - # "RGB" image contains 768 values). - #

- # A bilevel image (mode "1") is treated as a greyscale ("L") image - # by this method. - #

- # If a mask is provided, the method returns a histogram for those - # parts of the image where the mask image is non-zero. The mask - # image must have the same size as the image, and be either a - # bi-level image (mode "1") or a greyscale image ("L"). - # - # @def histogram(mask=None) - # @param mask An optional mask. - # @return A list containing pixel counts. - - def histogram(self, mask=None, extrema=None): - "Take histogram of image" - - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - ## - # (Deprecated) Returns a copy of the image where the data has been - # offset by the given distances. Data wraps around the edges. If - # yoffset is omitted, it is assumed to be equal to xoffset. - #

- # This method is deprecated. New code should use the offset - # function in the ImageChops module. - # - # @param xoffset The horizontal distance. - # @param yoffset The vertical distance. If omitted, both - # distances are set to the same value. - # @return An Image object. - - def offset(self, xoffset, yoffset=None): - "(deprecated) Offset image in horizontal and/or vertical direction" - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - import ImageChops - return ImageChops.offset(self, xoffset, yoffset) - - ## - # Pastes another image into this image. The box argument is either - # a 2-tuple giving the upper left corner, a 4-tuple defining the - # left, upper, right, and lower pixel coordinate, or None (same as - # (0, 0)). If a 4-tuple is given, the size of the pasted image - # must match the size of the region. - #

- # If the modes don't match, the pasted image is converted to the - # mode of this image (see the {@link #Image.convert} method for - # details). - #

- # Instead of an image, the source can be a integer or tuple - # containing pixel values. The method then fills the region - # with the given colour. When creating RGB images, you can - # also use colour strings as supported by the ImageColor module. - #

- # If a mask is given, this method updates only the regions - # indicated by the mask. You can use either "1", "L" or "RGBA" - # images (in the latter case, the alpha band is used as mask). - # Where the mask is 255, the given image is copied as is. Where - # the mask is 0, the current value is preserved. Intermediate - # values can be used for transparency effects. - #

- # Note that if you paste an "RGBA" image, the alpha band is - # ignored. You can work around this by using the same image as - # both source image and mask. - # - # @param im Source image or pixel value (integer or tuple). - # @param box An optional 4-tuple giving the region to paste into. - # If a 2-tuple is used instead, it's treated as the upper left - # corner. If omitted or None, the source is pasted into the - # upper left corner. - #

- # If an image is given as the second argument and there is no - # third, the box defaults to (0, 0), and the second argument - # is interpreted as a mask image. - # @param mask An optional mask image. - # @return An Image object. - - def paste(self, im, box=None, mask=None): - "Paste other image into region" - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box; box = None - - if box is None: - # cover all of self - box = (0, 0) + self.size - - if len(box) == 2: - # lower left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box = box + (box[0]+size[0], box[1]+size[1]) - - if isStringType(im): - import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self.load() - if self.readonly: - self._copy() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - ## - # Maps this image through a lookup table or function. - # - # @param lut A lookup table, containing 256 values per band in the - # image. A function can be used instead, it should take a single - # argument. The function is called once for each possible pixel - # value, and the resulting table is applied to all bands of the - # image. - # @param mode Output mode (default is same as input). In the - # current version, this can only be used if the source image - # has mode "L" or "P", and the output has mode "1". - # @return An Image object. - - def point(self, lut, mode=None): - "Map image through lookup table" - - self.load() - - if not isSequenceType(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = map(lut, range(256)) * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") - - return self._new(self.im.point(lut, mode)) - - ## - # Adds or replaces the alpha layer in this image. If the image - # does not have an alpha layer, it's converted to "LA" or "RGBA". - # The new layer must be either "L" or "1". - # - # @param im The new alpha layer. This can either be an "L" or "1" - # image having the same size as this image, or an integer or - # other color value. - - def putalpha(self, alpha): - "Set alpha layer" - - self.load() - if self.readonly: - self._copy() - - if self.mode not in ("LA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError): - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check - self.im = im - self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") - - if self.mode == "LA": - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - ## - # Copies pixel data to this image. This method copies data from a - # sequence object into the image, starting at the upper left - # corner (0, 0), and continuing until either the image or the - # sequence ends. The scale and offset values are used to adjust - # the sequence values: pixel = value*scale + offset. - # - # @param data A sequence object. - # @param scale An optional scale value. The default is 1.0. - # @param offset An optional offset value. The default is 0.0. - - def putdata(self, data, scale=1.0, offset=0.0): - "Put data from a sequence object into an image." - - self.load() - if self.readonly: - self._copy() - - self.im.putdata(data, scale, offset) - - ## - # Attaches a palette to this image. The image must be a "P" or - # "L" image, and the palette sequence must contain 768 integer - # values, where each group of three values represent the red, - # green, and blue values for the corresponding pixel - # index. Instead of an integer sequence, you can use an 8-bit - # string. - # - # @def putpalette(data) - # @param data A palette sequence (either a list or a string). - - def putpalette(self, data, rawmode="RGB"): - "Put palette data into an image." - - self.load() - if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") - if not isStringType(data): - data = string.join(map(chr, data), "") - self.mode = "P" - self.palette = ImagePalette.raw(rawmode, data) - self.palette.mode = "RGB" - self.load() # install new palette - - ## - # Modifies the pixel at the given position. The colour is given as - # a single numerical value for single-band images, and a tuple for - # multi-band images. - #

- # Note that this method is relatively slow. For more extensive - # changes, use {@link #Image.paste} or the ImageDraw module - # instead. - # - # @param xy The pixel coordinate, given as (x, y). - # @param value The pixel value. - # @see #Image.paste - # @see #Image.putdata - # @see ImageDraw - - def putpixel(self, xy, value): - "Set pixel value" - - self.load() - if self.readonly: - self._copy() - - return self.im.putpixel(xy, value) - - ## - # Returns a resized copy of this image. - # - # @def resize(size, filter=NEAREST) - # @param size The requested size in pixels, as a 2-tuple: - # (width, height). - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), BICUBIC - # (cubic spline interpolation in a 4x4 environment), or - # ANTIALIAS (a high-quality downsampling filter). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @return An Image object. - - def resize(self, size, resample=NEAREST): - "Resize image" - - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) - - ## - # Returns a rotated copy of this image. This method returns a - # copy of this image, rotated the given number of degrees counter - # clockwise around its centre. - # - # @def rotate(angle, filter=NEAREST) - # @param angle In degrees counter clockwise. - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or BICUBIC - # (cubic spline interpolation in a 4x4 environment). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @param expand Optional expansion flag. If true, expands the output - # image to make it large enough to hold the entire rotated image. - # If false or omitted, make the output image the same size as the - # input image. - # @return An Image object. - - def rotate(self, angle, resample=NEAREST, expand=0): - "Rotate image. Angle given as degrees counter-clockwise." - - if expand: - import math - angle = -angle * math.pi / 180 - matrix = [ - math.cos(angle), math.sin(angle), 0.0, - -math.sin(angle), math.cos(angle), 0.0 - ] - def transform(x, y, (a, b, c, d, e, f)=matrix): - return a*x + b*y + c, d*x + e*y + f - - # calculate output size - w, h = self.size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) - xx.append(x) - yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) - - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y - - return self.transform((w, h), AFFINE, matrix) - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - return self._new(self.im.rotate(angle, resample)) - - ## - # Saves this image under the given filename. If no format is - # specified, the format to use is determined from the filename - # extension, if possible. - #

- # Keyword options can be used to provide additional instructions - # to the writer. If a writer doesn't recognise an option, it is - # silently ignored. The available options are described later in - # this handbook. - #

- # You can use a file object instead of a filename. In this case, - # you must always specify the format. The file object must - # implement the seek, tell, and write - # methods, and be opened in binary mode. - # - # @def save(file, format=None, **options) - # @param file File name or file object. - # @param format Optional format override. If omitted, the - # format to use is determined from the filename extension. - # If a file object was used instead of a filename, this - # parameter should always be used. - # @param **options Extra parameters to the image writer. - # @return None - # @exception KeyError If the output format could not be determined - # from the file name. Use the format option to solve this. - # @exception IOError If the file could not be written. The file - # may have been created, and may contain partial data. - - def save(self, fp, format=None, **params): - "Save image to file or stream" - - if isStringType(fp): - filename = fp - else: - if hasattr(fp, "name") and isStringType(fp.name): - filename = fp.name - else: - filename = "" - - # may mutate self! - self.load() - - self.encoderinfo = params - self.encoderconfig = () - - preinit() - - ext = string.lower(os.path.splitext(filename)[1]) - - if not format: - try: - format = EXTENSION[ext] - except KeyError: - init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension - - try: - save_handler = SAVE[string.upper(format)] - except KeyError: - init() - save_handler = SAVE[string.upper(format)] # unknown format - - if isStringType(fp): - import __builtin__ - fp = __builtin__.open(fp, "wb") - close = 1 - else: - close = 0 - - try: - save_handler(self, fp, filename) - finally: - # do what we can to clean up - if close: - fp.close() - - ## - # Seeks to the given frame in this sequence file. If you seek - # beyond the end of the sequence, the method raises an - # EOFError exception. When a sequence file is opened, the - # library automatically seeks to frame 0. - #

- # Note that in the current version of the library, most sequence - # formats only allows you to seek to the next frame. - # - # @param frame Frame number, starting at 0. - # @exception EOFError If the call attempts to seek beyond the end - # of the sequence. - # @see #Image.tell - - def seek(self, frame): - "Seek to given frame in sequence file" - - # overridden by file handlers - if frame != 0: - raise EOFError - - ## - # Displays this image. This method is mainly intended for - # debugging purposes. - #

- # On Unix platforms, this method saves the image to a temporary - # PPM file, and calls the xv utility. - #

- # On Windows, it saves the image to a temporary BMP file, and uses - # the standard BMP display utility to show it (usually Paint). - # - # @def show(title=None) - # @param title Optional title to use for the image window, - # where possible. - - def show(self, title=None, command=None): - "Display image (for debug purposes only)" - - _showxv(self, title, command) - - ## - # Split this image into individual bands. This method returns a - # tuple of individual image bands from an image. For example, - # splitting an "RGB" image creates three new images each - # containing a copy of one of the original bands (red, green, - # blue). - # - # @return A tuple containing bands. - - def split(self): - "Split image into bands" - - ims = [] - self.load() - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) - return tuple(ims) - - ## - # Returns the current frame number. - # - # @return Frame number, starting with 0. - # @see #Image.seek - - def tell(self): - "Return current frame number" - - return 0 - - ## - # Make this image into a thumbnail. This method modifies the - # image to contain a thumbnail version of itself, no larger than - # the given size. This method calculates an appropriate thumbnail - # size to preserve the aspect of the image, calls the {@link - # #Image.draft} method to configure the file reader (where - # applicable), and finally resizes the image. - #

- # Note that the bilinear and bicubic filters in the current - # version of PIL are not well-suited for thumbnail generation. - # You should use ANTIALIAS unless speed is much more - # important than quality. - #

- # Also note that this function modifies the Image object in place. - # If you need to use the full resolution image as well, apply this - # method to a {@link #Image.copy} of the original image. - # - # @param size Requested size. - # @param resample Optional resampling filter. This can be one - # of NEAREST, BILINEAR, BICUBIC, or - # ANTIALIAS (best quality). If omitted, it defaults - # to NEAREST (this will be changed to ANTIALIAS in a - # future version). - # @return None - - def thumbnail(self, size, resample=NEAREST): - "Create thumbnail representation (modifies image in place)" - - # FIXME: the default resampling filter will be changed - # to ANTIALIAS in future versions - - # preserve aspect ratio - x, y = self.size - if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] - if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] - size = x, y - - if size == self.size: - return - - self.draft(None, size) - - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback - - self.im = im.im - self.mode = im.mode - self.size = size - - self.readonly = 0 - - # FIXME: the different tranform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - - ## - # Transforms this image. This method creates a new image with the - # given size, and the same mode as the original, and copies data - # to the new image using the given transform. - #

- # @def transform(size, method, data, resample=NEAREST) - # @param size The output size. - # @param method The transformation method. This is one of - # EXTENT (cut out a rectangular subregion), AFFINE - # (affine transform), PERSPECTIVE (perspective - # transform), QUAD (map a quadrilateral to a - # rectangle), or MESH (map a number of source quadrilaterals - # in one operation). - # @param data Extra data to the transformation method. - # @param resample Optional resampling filter. It can be one of - # NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or - # BICUBIC (cubic spline interpolation in a 4x4 - # environment). If omitted, or if the image has mode - # "1" or "P", it is set to NEAREST. - # @return An Image object. - - def transform(self, size, method, data=None, resample=NEAREST, fill=1): - "Transform image" - - import ImageTransform - if isinstance(method, ImageTransform.Transform): - method, data = method.getdata() - if data is None: - raise ValueError("missing method data") - im = new(self.mode, size, None) - if method == MESH: - # list of quads - for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) - else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) - - return im - - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): - - # FIXME: this should be turned into a lazy operation (?) - - w = box[2]-box[0] - h = box[3]-box[1] - - if method == AFFINE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4]) - elif method == EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE - data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) - elif method == PERSPECTIVE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4], - data[6], data[7]) - elif method == QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) - else: - raise ValueError("unknown transformation method") - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = NEAREST - - self.im.transform2(box, image.im, method, data, resample, fill) - - ## - # Returns a flipped or rotated copy of this image. - # - # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, - # ROTATE_90, ROTATE_180, or ROTATE_270. - - def transpose(self, method): - "Transpose image (flip or rotate in 90 degree steps)" - - self.load() - im = self.im.transpose(method) - return self._new(im) - -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - x0, y0, x1, y1 = box - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - # FIXME: future versions should optimize crop/paste - # sequences! - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - -def _wedge(): - "Create greyscale wedge (for debugging only)" - - return Image()._new(core.wedge("L")) - -## -# Creates a new image with the given mode and size. -# -# @param mode The mode to use for the new image. -# @param size A 2-tuple, containing (width, height) in pixels. -# @param color What colour to use for the image. Default is black. -# If given, this should be a single integer or floating point value -# for single-band modes, and a tuple for multi-band modes (one value -# per band). When creating RGB images, you can also use colour -# strings as supported by the ImageColor module. If the colour is -# None, the image is not initialised. -# @return An Image object. - -def new(mode, size, color=0): - "Create a new image" - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isStringType(color): - # css3-style specifier - - import ImageColor - color = ImageColor.getcolor(color, mode) - - return Image()._new(core.fill(mode, size, color)) - -## -# Creates an image memory from pixel data in a string. -#

-# In its simplest form, this function takes three arguments -# (mode, size, and unpacked pixel data). -#

-# You can also use any pixel decoder supported by PIL. For more -# information on available decoders, see the section Writing Your Own File Decoder. -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string containing raw data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. -# @return An Image object. - -def fromstring(mode, size, data, decoder_name="raw", *args): - "Load image from string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im = new(mode, size) - im.fromstring(data, decoder_name, args) - return im - -## -# (New in 1.1.4) Creates an image memory from pixel data in a string -# or byte buffer. -#

-# This function is similar to {@link #fromstring}, but uses data in -# the byte buffer, where possible. This means that changes to the -# original buffer object are reflected in this image). Not all modes -# can share memory; supported modes include "L", "RGBX", "RGBA", and -# "CMYK". -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image file in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -#

-# In the current version, the default parameters used for the "raw" -# decoder differs from that used for {@link fromstring}. This is a -# bug, and will probably be fixed in a future release. The current -# release issues a warning if you do this; to disable the warning, -# you should provide the full set of parameters. See below for -# details. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string or other buffer object containing raw -# data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. For the -# default encoder ("raw"), it's recommended that you provide the -# full set of parameters: -# frombuffer(mode, size, data, "raw", mode, 0, 1). -# @return An Image object. -# @since 1.1.4 - -def frombuffer(mode, size, data, decoder_name="raw", *args): - "Load image from string or buffer" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw": - if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 - if args[0] in _MAPMODES: - im = new(mode, (1,1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) - im.readonly = 1 - return im - - return apply(fromstring, (mode, size, data, decoder_name, args)) - - -## -# (New in 1.1.6) Create an image memory from an object exporting -# the array interface (using the buffer protocol). -# -# If obj is not contiguous, then the tostring method is called -# and {@link frombuffer} is used. -# -# @param obj Object with array interface -# @param mode Mode to use (will be determined from type if None) -# @return An image memory. - -def fromarray(obj, mode=None): - arr = obj.__array_interface__ - shape = arr['shape'] - ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None - if mode is None: - typestr = arr['typestr'] - if not (typestr[0] == '|' or typestr[0] == _ENDIAN or - typestr[1:] not in ['u1', 'b1', 'i4', 'f4']): - raise TypeError("cannot handle data-type") - if typestr[0] == _ENDIAN: - typestr = typestr[1:3] - else: - typestr = typestr[:2] - if typestr == 'i4': - mode = 'I' - if typestr == 'u2': - mode = 'I;16' - elif typestr == 'f4': - mode = 'F' - elif typestr == 'b1': - mode = '1' - elif ndim == 2: - mode = 'L' - elif ndim == 3: - mode = 'RGB' - elif ndim == 4: - mode = 'RGBA' - else: - raise TypeError("Do not understand data.") - ndmax = 4 - bad_dims=0 - if mode in ['1','L','I','P','F']: - ndmax = 2 - elif mode == 'RGB': - ndmax = 3 - if ndim > ndmax: - raise ValueError("Too many dimensions.") - - size = shape[:2][::-1] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", mode, 0, 1) - -## -# Opens and identifies the given image file. -#

-# This is a lazy operation; this function identifies the file, but the -# actual image data is not read from the file until you try to process -# the data (or call the {@link #Image.load} method). -# -# @def open(file, mode="r") -# @param file A filename (string) or a file object. The file object -# must implement read, seek, and tell methods, -# and be opened in binary mode. -# @param mode The mode. If given, this argument must be "r". -# @return An Image object. -# @exception IOError If the file cannot be found, or the image cannot be -# opened and identified. -# @see #new - -def open(fp, mode="r"): - "Open an image file, without loading the raster data" - - if mode != "r": - raise ValueError("bad mode") - - if isStringType(fp): - import __builtin__ - filename = fp - fp = __builtin__.open(fp, "rb") - else: - filename = "" - - prefix = fp.read(16) - - preinit() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - init() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - raise IOError("cannot identify image file") - -# -# Image processing. - -## -# Creates a new image by interpolating between two input images, using -# a constant alpha. -# -#

-#    out = image1 * (1.0 - alpha) + image2 * alpha
-# 
-# -# @param im1 The first image. -# @param im2 The second image. Must have the same mode and size as -# the first image. -# @param alpha The interpolation alpha factor. If alpha is 0.0, a -# copy of the first image is returned. If alpha is 1.0, a copy of -# the second image is returned. There are no restrictions on the -# alpha value. If necessary, the result is clipped to fit into -# the allowed output range. -# @return An Image object. - -def blend(im1, im2, alpha): - "Interpolate between images." - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - -## -# Creates a new image by interpolating between two input images, -# using the mask as alpha. -# -# @param image1 The first image. -# @param image2 The second image. Must have the same mode and -# size as the first image. -# @param mask A mask image. This image can can have mode -# "1", "L", or "RGBA", and must have the same size as the -# other two images. - -def composite(image1, image2, mask): - "Create composite image by blending images using a transparency mask" - - image = image2.copy() - image.paste(image1, None, mask) - return image - -## -# Applies the function (which should take one argument) to each pixel -# in the given image. If the image has more than one band, the same -# function is applied to each band. Note that the function is -# evaluated once for each possible pixel value, so you cannot use -# random components or other generators. -# -# @def eval(image, function) -# @param image The input image. -# @param function A function object, taking one integer argument. -# @return An Image object. - -def eval(image, *args): - "Evaluate image expression" - - return image.point(args[0]) - -## -# Creates a new image from a number of single-band images. -# -# @param mode The mode to use for the output image. -# @param bands A sequence containing one single-band image for -# each band in the output image. All bands must have the -# same size. -# @return An Image object. - -def merge(mode, bands): - "Merge a set of single band images into a new multiband image." - - if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): - raise ValueError("mode mismatch") - if im.size != bands[0].size: - raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) - -# -------------------------------------------------------------------- -# Plugin registry - -## -# Register an image file plugin. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param factory An image file factory method. -# @param accept An optional function that can be used to quickly -# reject images having another format. - -def register_open(id, factory, accept=None): - id = string.upper(id) - ID.append(id) - OPEN[id] = factory, accept - -## -# Registers an image MIME type. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param mimetype The image MIME type for this format. - -def register_mime(id, mimetype): - MIME[string.upper(id)] = mimetype - -## -# Registers an image save function. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param driver A function to save images in this format. - -def register_save(id, driver): - SAVE[string.upper(id)] = driver - -## -# Registers an image extension. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param extension An extension used for this format. - -def register_extension(id, extension): - EXTENSION[string.lower(extension)] = string.upper(id) - - -# -------------------------------------------------------------------- -# Simple display support - -def _showxv(image, title=None, command=None): - - if os.name == "nt": - format = "BMP" - elif sys.platform == "darwin": - format = "JPEG" - if not command: - command = "open -a /Applications/Preview.app" - else: - format = None - if not command: - command = "xv" - if title: - command = command + " -name \"%s\"" % title - - if image.mode == "I;16": - # @PIL88 @PIL101 - # "I;16" isn't an 'official' mode, but we still want to - # provide a simple way to show 16-bit images. - base = "L" - else: - base = getmodebase(image.mode) - if base != image.mode and image.mode != "1": - file = image.convert(base)._dump(format=format) - else: - file = image._dump(format=format) - - if os.name == "nt": - command = "start /wait %s && del /f %s" % (file, file) - elif sys.platform == "darwin": - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) - else: - command = "(%s %s; rm -f %s)&" % (command, file, file) - - os.system(command) diff --git a/pyqtgraph/PIL_Fix/Image.py-1.7 b/pyqtgraph/PIL_Fix/Image.py-1.7 deleted file mode 100644 index cacbcc643f..0000000000 --- a/pyqtgraph/PIL_Fix/Image.py-1.7 +++ /dev/null @@ -1,2129 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# the Image class wrapper -# -# partial release history: -# 1995-09-09 fl Created -# 1996-03-11 fl PIL release 0.0 (proof of concept) -# 1996-04-30 fl PIL release 0.1b1 -# 1999-07-28 fl PIL release 1.0 final -# 2000-06-07 fl PIL release 1.1 -# 2000-10-20 fl PIL release 1.1.1 -# 2001-05-07 fl PIL release 1.1.2 -# 2002-03-15 fl PIL release 1.1.3 -# 2003-05-10 fl PIL release 1.1.4 -# 2005-03-28 fl PIL release 1.1.5 -# 2006-12-02 fl PIL release 1.1.6 -# 2009-11-15 fl PIL release 1.1.7 -# -# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. -# Copyright (c) 1995-2009 by Fredrik Lundh. -# -# See the README file for information on usage and redistribution. -# - -VERSION = "1.1.7" - -try: - import warnings -except ImportError: - warnings = None - -class _imaging_not_installed: - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - -try: - # give Tk a chance to set up the environment, in case we're - # using an _imaging module linked against libtcl/libtk (use - # __import__ to hide this from naive packagers; we don't really - # depend on Tk unless ImageTk is used, and that module already - # imports Tkinter) - __import__("FixTk") -except ImportError: - pass - -try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. - import _imaging - core = _imaging - del _imaging -except ImportError, v: - core = _imaging_not_installed() - if str(v)[:20] == "Module use of python" and warnings: - # The _imaging C module is present, but not compiled for - # the right version (windows only). Print a warning, if - # possible. - warnings.warn( - "The _imaging extension was built for another version " - "of Python; most PIL functions will be disabled", - RuntimeWarning - ) - -import ImageMode -import ImagePalette - -import os, string, sys - -# type stuff -from types import IntType, StringType, TupleType - -try: - UnicodeStringType = type(unicode("")) - ## - # (Internal) Checks if an object is a string. If the current - # Python version supports Unicode, this checks for both 8-bit - # and Unicode strings. - def isStringType(t): - return isinstance(t, StringType) or isinstance(t, UnicodeStringType) -except NameError: - def isStringType(t): - return isinstance(t, StringType) - -## -# (Internal) Checks if an object is a tuple. - -def isTupleType(t): - return isinstance(t, TupleType) - -## -# (Internal) Checks if an object is an image object. - -def isImageType(t): - return hasattr(t, "im") - -## -# (Internal) Checks if an object is a string, and that it points to a -# directory. - -def isDirectory(f): - return isStringType(f) and os.path.isdir(f) - -from operator import isNumberType, isSequenceType - -# -# Debug level - -DEBUG = 0 - -# -# Constants (also defined in _imagingmodule.c!) - -NONE = 0 - -# transpose -FLIP_LEFT_RIGHT = 0 -FLIP_TOP_BOTTOM = 1 -ROTATE_90 = 2 -ROTATE_180 = 3 -ROTATE_270 = 4 - -# transforms -AFFINE = 0 -EXTENT = 1 -PERSPECTIVE = 2 -QUAD = 3 -MESH = 4 - -# resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 - -# dithers -NONE = 0 -NEAREST = 0 -ORDERED = 1 # Not yet implemented -RASTERIZE = 2 # Not yet implemented -FLOYDSTEINBERG = 3 # default - -# palettes/quantizers -WEB = 0 -ADAPTIVE = 1 - -# categories -NORMAL = 0 -SEQUENCE = 1 -CONTAINER = 2 - -# -------------------------------------------------------------------- -# Registries - -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -EXTENSION = {} - -# -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... - -} - -try: - byteorder = sys.byteorder -except AttributeError: - import struct - if struct.unpack("h", "\0\1")[0] == 1: - byteorder = "big" - else: - byteorder = "little" - -if byteorder == 'little': - _ENDIAN = '<' -else: - _ENDIAN = '>' - -_MODE_CONV = { - # official modes - "1": ('|b1', None), # broken - "L": ('|u1', None), - "I": (_ENDIAN + 'i4', None), - "I;16": ('%su2' % _ENDIAN, None), - "F": (_ENDIAN + 'f4', None), - "P": ('|u1', None), - "RGB": ('|u1', 3), - "RGBX": ('|u1', 4), - "RGBA": ('|u1', 4), - "CMYK": ('|u1', 4), - "YCbCr": ('|u1', 4), -} - -def _conv_type_shape(im): - shape = im.size[1], im.size[0] - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return shape, typ - else: - return shape+(extra,), typ - - -MODES = _MODEINFO.keys() -MODES.sort() - -# raw modes that may be memory mapped. NOTE: if you change this, you -# may have to modify the stride calculation in map.c too! -_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") - -## -# Gets the "base" mode for given mode. This function returns "L" for -# images that contain grayscale data, and "RGB" for images that -# contain color data. -# -# @param mode Input mode. -# @return "L" or "RGB". -# @exception KeyError If the input mode was not a standard mode. - -def getmodebase(mode): - return ImageMode.getmode(mode).basemode - -## -# Gets the storage type mode. Given a mode, this function returns a -# single-layer mode suitable for storing individual bands. -# -# @param mode Input mode. -# @return "L", "I", or "F". -# @exception KeyError If the input mode was not a standard mode. - -def getmodetype(mode): - return ImageMode.getmode(mode).basetype - -## -# Gets a list of individual band names. Given a mode, this function -# returns a tuple containing the names of individual bands (use -# {@link #getmodetype} to get the mode used to store each individual -# band. -# -# @param mode Input mode. -# @return A tuple containing band names. The length of the tuple -# gives the number of bands in an image of the given mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebandnames(mode): - return ImageMode.getmode(mode).bands - -## -# Gets the number of individual bands for this mode. -# -# @param mode Input mode. -# @return The number of bands in this mode. -# @exception KeyError If the input mode was not a standard mode. - -def getmodebands(mode): - return len(ImageMode.getmode(mode).bands) - -# -------------------------------------------------------------------- -# Helpers - -_initialized = 0 - -## -# Explicitly loads standard file format drivers. - -def preinit(): - "Load standard file format drivers." - - global _initialized - if _initialized >= 1: - return - - try: - import BmpImagePlugin - except ImportError: - pass - try: - import GifImagePlugin - except ImportError: - pass - try: - import JpegImagePlugin - except ImportError: - pass - try: - import PpmImagePlugin - except ImportError: - pass - try: - import PngImagePlugin - except ImportError: - pass -# try: -# import TiffImagePlugin -# except ImportError: -# pass - - _initialized = 1 - -## -# Explicitly initializes the Python Imaging Library. This function -# loads all available file format drivers. - -def init(): - "Load all file format drivers." - - global _initialized - if _initialized >= 2: - return 0 - - visited = {} - - directories = sys.path - - try: - directories = directories + [os.path.dirname(__file__)] - except NameError: - pass - - # only check directories (including current, if present in the path) - for directory in filter(isDirectory, directories): - fullpath = os.path.abspath(directory) - if visited.has_key(fullpath): - continue - for file in os.listdir(directory): - if file[-14:] == "ImagePlugin.py": - f, e = os.path.splitext(file) - try: - sys.path.insert(0, directory) - try: - __import__(f, globals(), locals(), []) - finally: - del sys.path[0] - except ImportError: - if DEBUG: - print "Image: failed to import", - print f, ":", sys.exc_value - visited[fullpath] = None - - if OPEN or SAVE: - _initialized = 2 - return 1 - -# -------------------------------------------------------------------- -# Codec factories (used by tostring/fromstring and ImageFile.load) - -def _getdecoder(mode, decoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get decoder - decoder = getattr(core, decoder_name + "_decoder") - # print decoder, (mode,) + args + extra - return apply(decoder, (mode,) + args + extra) - except AttributeError: - raise IOError("decoder %s not available" % decoder_name) - -def _getencoder(mode, encoder_name, args, extra=()): - - # tweak arguments - if args is None: - args = () - elif not isTupleType(args): - args = (args,) - - try: - # get encoder - encoder = getattr(core, encoder_name + "_encoder") - # print encoder, (mode,) + args + extra - return apply(encoder, (mode,) + args + extra) - except AttributeError: - raise IOError("encoder %s not available" % encoder_name) - - -# -------------------------------------------------------------------- -# Simple expression analyzer - -class _E: - def __init__(self, data): self.data = data - def __coerce__(self, other): return self, _E(other) - def __add__(self, other): return _E((self.data, "__add__", other.data)) - def __mul__(self, other): return _E((self.data, "__mul__", other.data)) - -def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if (a is stub and b == "__mul__" and isNumberType(c)): - return c, 0.0 - if (a is stub and b == "__add__" and isNumberType(c)): - return 1.0, c - except TypeError: pass - try: - ((a, b, c), d, e) = data # full syntax - if (a is stub and b == "__mul__" and isNumberType(c) and - d == "__add__" and isNumberType(e)): - return c, e - except TypeError: pass - raise ValueError("illegal expression") - - -# -------------------------------------------------------------------- -# Implementation wrapper - -## -# This class represents an image object. To create Image objects, use -# the appropriate factory functions. There's hardly ever any reason -# to call the Image constructor directly. -# -# @see #open -# @see #new -# @see #fromstring - -class Image: - - format = None - format_description = None - - def __init__(self): - # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? - self.im = None - self.mode = "" - self.size = (0, 0) - self.palette = None - self.info = {} - self.category = NORMAL - self.readonly = 0 - - def _new(self, im): - new = Image() - new.im = im - new.mode = im.mode - new.size = im.size - new.palette = self.palette - if im.mode == "P": - new.palette = ImagePalette.ImagePalette() - try: - new.info = self.info.copy() - except AttributeError: - # fallback (pre-1.5.2) - new.info = {} - for k, v in self.info: - new.info[k] = v - return new - - _makeself = _new # compatibility - - def _copy(self): - self.load() - self.im = self.im.copy() - self.readonly = 0 - - def _dump(self, file=None, format=None): - import tempfile - if not file: - file = tempfile.mktemp() - self.load() - if not format or format == "PPM": - self.im.save_ppm(file) - else: - file = file + "." + format - self.save(file, format) - return file - - def __repr__(self): - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, self.__class__.__name__, - self.mode, self.size[0], self.size[1], - id(self) - ) - - def __getattr__(self, name): - if name == "__array_interface__": - # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new['shape'] = shape - new['typestr'] = typestr - new['data'] = self.tostring() - return new - raise AttributeError(name) - - ## - # Returns a string containing pixel data. - # - # @param encoder_name What encoder to use. The default is to - # use the standard "raw" encoder. - # @param *args Extra arguments to the encoder. - # @return An 8-bit string. - - def tostring(self, encoder_name="raw", *args): - "Return image as a binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if encoder_name == "raw" and args == (): - args = self.mode - - self.load() - - # unpack data - e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) - - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - - data = [] - while 1: - l, s, d = e.encode(bufsize) - data.append(d) - if s: - break - if s < 0: - raise RuntimeError("encoder error %d in tostring" % s) - - return string.join(data, "") - - ## - # Returns the image converted to an X11 bitmap. This method - # only works for mode "1" images. - # - # @param name The name prefix to use for the bitmap variables. - # @return A string containing an X11 bitmap. - # @exception ValueError If the mode is not "1" - - def tobitmap(self, name="image"): - "Return image as an XBM bitmap" - - self.load() - if self.mode != "1": - raise ValueError("not a bitmap") - data = self.tostring("xbm") - return string.join(["#define %s_width %d\n" % (name, self.size[0]), - "#define %s_height %d\n"% (name, self.size[1]), - "static char %s_bits[] = {\n" % name, data, "};"], "") - - ## - # Loads this image with pixel data from a string. - #

- # This method is similar to the {@link #fromstring} function, but - # loads data into this image instead of creating a new image - # object. - - def fromstring(self, data, decoder_name="raw", *args): - "Load data to image from binary string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - # default format - if decoder_name == "raw" and args == (): - args = self.mode - - # unpack data - d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) - s = d.decode(data) - - if s[0] >= 0: - raise ValueError("not enough image data") - if s[1] != 0: - raise ValueError("cannot decode image data") - - ## - # Allocates storage for the image and loads the pixel data. In - # normal cases, you don't need to call this method, since the - # Image class automatically loads an opened image when it is - # accessed for the first time. - # - # @return An image access object. - - def load(self): - "Explicitly load pixel data." - if self.im and self.palette and self.palette.dirty: - # realize palette - apply(self.im.putpalette, self.palette.getdata()) - self.palette.dirty = 0 - self.palette.mode = "RGB" - self.palette.rawmode = None - if self.info.has_key("transparency"): - self.im.putpalettealpha(self.info["transparency"], 0) - self.palette.mode = "RGBA" - if self.im: - return self.im.pixel_access(self.readonly) - - ## - # Verifies the contents of a file. For data read from a file, this - # method attempts to determine if the file is broken, without - # actually decoding the image data. If this method finds any - # problems, it raises suitable exceptions. If you need to load - # the image after using this method, you must reopen the image - # file. - - def verify(self): - "Verify file contents." - pass - - ## - # Returns a converted copy of this image. For the "P" mode, this - # method translates pixels through the palette. If mode is - # omitted, a mode is chosen so that all information in the image - # and the palette can be represented without a palette. - #

- # The current version supports all possible conversions between - # "L", "RGB" and "CMYK." - #

- # When translating a colour image to black and white (mode "L"), - # the library uses the ITU-R 601-2 luma transform: - #

- # L = R * 299/1000 + G * 587/1000 + B * 114/1000 - #

- # When translating a greyscale image into a bilevel image (mode - # "1"), all non-zero values are set to 255 (white). To use other - # thresholds, use the {@link #Image.point} method. - # - # @def convert(mode, matrix=None, **options) - # @param mode The requested mode. - # @param matrix An optional conversion matrix. If given, this - # should be 4- or 16-tuple containing floating point values. - # @param options Additional options, given as keyword arguments. - # @keyparam dither Dithering method, used when converting from - # mode "RGB" to "P". - # Available methods are NONE or FLOYDSTEINBERG (default). - # @keyparam palette Palette to use when converting from mode "RGB" - # to "P". Available palettes are WEB or ADAPTIVE. - # @keyparam colors Number of colors to use for the ADAPTIVE palette. - # Defaults to 256. - # @return An Image object. - - def convert(self, mode=None, data=None, dither=None, - palette=WEB, colors=256): - "Convert to other pixel format" - - if not mode: - # determine default mode - if self.mode == "P": - self.load() - if self.palette: - mode = self.palette.mode - else: - mode = "RGB" - else: - return self.copy() - - self.load() - - if data: - # matrix conversion - if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") - im = self.im.convert_matrix(mode, data) - return self._new(im) - - if mode == "P" and palette == ADAPTIVE: - im = self.im.quantize(colors) - return self._new(im) - - # colourspace conversion - if dither is None: - dither = FLOYDSTEINBERG - - try: - im = self.im.convert(mode, dither) - except ValueError: - try: - # normalize source image and try again - im = self.im.convert(getmodebase(self.mode)) - im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") - - return self._new(im) - - def quantize(self, colors=256, method=0, kmeans=0, palette=None): - - # methods: - # 0 = median cut - # 1 = maximum coverage - - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. - - self.load() - - if palette: - # use palette from reference image - palette.load() - if palette.mode != "P": - raise ValueError("bad mode for palette image") - if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) - im = self.im.convert("P", 1, palette.im) - return self._makeself(im) - - im = self.im.quantize(colors, method, kmeans) - return self._new(im) - - ## - # Copies this image. Use this method if you wish to paste things - # into an image, but still retain the original. - # - # @return An Image object. - - def copy(self): - "Copy raster data" - - self.load() - im = self.im.copy() - return self._new(im) - - ## - # Returns a rectangular region from this image. The box is a - # 4-tuple defining the left, upper, right, and lower pixel - # coordinate. - #

- # This is a lazy operation. Changes to the source image may or - # may not be reflected in the cropped image. To break the - # connection, call the {@link #Image.load} method on the cropped - # copy. - # - # @param The crop rectangle, as a (left, upper, right, lower)-tuple. - # @return An Image object. - - def crop(self, box=None): - "Crop region from image" - - self.load() - if box is None: - return self.copy() - - # lazy operation - return _ImageCrop(self, box) - - ## - # Configures the image file loader so it returns a version of the - # image that as closely as possible matches the given mode and - # size. For example, you can use this method to convert a colour - # JPEG to greyscale while loading it, or to extract a 128x192 - # version from a PCD file. - #

- # Note that this method modifies the Image object in place. If - # the image has already been loaded, this method has no effect. - # - # @param mode The requested mode. - # @param size The requested size. - - def draft(self, mode, size): - "Configure image decoder" - - pass - - def _expand(self, xmargin, ymargin=None): - if ymargin is None: - ymargin = xmargin - self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) - - ## - # Filters this image using the given filter. For a list of - # available filters, see the ImageFilter module. - # - # @param filter Filter kernel. - # @return An Image object. - # @see ImageFilter - - def filter(self, filter): - "Apply environment filter to image" - - self.load() - - if callable(filter): - filter = filter() - if not hasattr(filter, "filter"): - raise TypeError("filter argument should be ImageFilter.Filter instance or class") - - if self.im.bands == 1: - return self._new(filter.filter(self.im)) - # fix to handle multiband images since _imaging doesn't - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) - return merge(self.mode, ims) - - ## - # Returns a tuple containing the name of each band in this image. - # For example, getbands on an RGB image returns ("R", "G", "B"). - # - # @return A tuple containing band names. - - def getbands(self): - "Get band names" - - return ImageMode.getmode(self.mode).bands - - ## - # Calculates the bounding box of the non-zero regions in the - # image. - # - # @return The bounding box is returned as a 4-tuple defining the - # left, upper, right, and lower pixel coordinate. If the image - # is completely empty, this method returns None. - - def getbbox(self): - "Get bounding box of actual data (non-zero pixels) in image" - - self.load() - return self.im.getbbox() - - ## - # Returns a list of colors used in this image. - # - # @param maxcolors Maximum number of colors. If this number is - # exceeded, this method returns None. The default limit is - # 256 colors. - # @return An unsorted list of (count, pixel) values. - - def getcolors(self, maxcolors=256): - "Get colors from image, up to given limit" - - self.load() - if self.mode in ("1", "L", "P"): - h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) - if len(out) > maxcolors: - return None - return out - return self.im.getcolors(maxcolors) - - ## - # Returns the contents of this image as a sequence object - # containing pixel values. The sequence object is flattened, so - # that values for line one follow directly after the values of - # line zero, and so on. - #

- # Note that the sequence object returned by this method is an - # internal PIL data type, which only supports certain sequence - # operations. To convert it to an ordinary sequence (e.g. for - # printing), use list(im.getdata()). - # - # @param band What band to return. The default is to return - # all bands. To return a single band, pass in the index - # value (e.g. 0 to get the "R" band from an "RGB" image). - # @return A sequence-like object. - - def getdata(self, band = None): - "Get image data as sequence object." - - self.load() - if band is not None: - return self.im.getband(band) - return self.im # could be abused - - ## - # Gets the the minimum and maximum pixel values for each band in - # the image. - # - # @return For a single-band image, a 2-tuple containing the - # minimum and maximum pixel value. For a multi-band image, - # a tuple containing one 2-tuple for each band. - - def getextrema(self): - "Get min/max value" - - self.load() - if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) - return self.im.getextrema() - - ## - # Returns a PyCObject that points to the internal image memory. - # - # @return A PyCObject object. - - def getim(self): - "Get PyCObject pointer to internal image memory" - - self.load() - return self.im.ptr - - - ## - # Returns the image palette as a list. - # - # @return A list of color values [r, g, b, ...], or None if the - # image has no palette. - - def getpalette(self): - "Get palette contents." - - self.load() - try: - return map(ord, self.im.getpalette()) - except ValueError: - return None # no palette - - - ## - # Returns the pixel value at a given position. - # - # @param xy The coordinate, given as (x, y). - # @return The pixel value. If the image is a multi-layer image, - # this method returns a tuple. - - def getpixel(self, xy): - "Get pixel value" - - self.load() - return self.im.getpixel(xy) - - ## - # Returns the horizontal and vertical projection. - # - # @return Two sequences, indicating where there are non-zero - # pixels along the X-axis and the Y-axis, respectively. - - def getprojection(self): - "Get projection to x and y axes" - - self.load() - x, y = self.im.getprojection() - return map(ord, x), map(ord, y) - - ## - # Returns a histogram for the image. The histogram is returned as - # a list of pixel counts, one for each pixel value in the source - # image. If the image has more than one band, the histograms for - # all bands are concatenated (for example, the histogram for an - # "RGB" image contains 768 values). - #

- # A bilevel image (mode "1") is treated as a greyscale ("L") image - # by this method. - #

- # If a mask is provided, the method returns a histogram for those - # parts of the image where the mask image is non-zero. The mask - # image must have the same size as the image, and be either a - # bi-level image (mode "1") or a greyscale image ("L"). - # - # @def histogram(mask=None) - # @param mask An optional mask. - # @return A list containing pixel counts. - - def histogram(self, mask=None, extrema=None): - "Take histogram of image" - - self.load() - if mask: - mask.load() - return self.im.histogram((0, 0), mask.im) - if self.mode in ("I", "F"): - if extrema is None: - extrema = self.getextrema() - return self.im.histogram(extrema) - return self.im.histogram() - - ## - # (Deprecated) Returns a copy of the image where the data has been - # offset by the given distances. Data wraps around the edges. If - # yoffset is omitted, it is assumed to be equal to xoffset. - #

- # This method is deprecated. New code should use the offset - # function in the ImageChops module. - # - # @param xoffset The horizontal distance. - # @param yoffset The vertical distance. If omitted, both - # distances are set to the same value. - # @return An Image object. - - def offset(self, xoffset, yoffset=None): - "(deprecated) Offset image in horizontal and/or vertical direction" - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - import ImageChops - return ImageChops.offset(self, xoffset, yoffset) - - ## - # Pastes another image into this image. The box argument is either - # a 2-tuple giving the upper left corner, a 4-tuple defining the - # left, upper, right, and lower pixel coordinate, or None (same as - # (0, 0)). If a 4-tuple is given, the size of the pasted image - # must match the size of the region. - #

- # If the modes don't match, the pasted image is converted to the - # mode of this image (see the {@link #Image.convert} method for - # details). - #

- # Instead of an image, the source can be a integer or tuple - # containing pixel values. The method then fills the region - # with the given colour. When creating RGB images, you can - # also use colour strings as supported by the ImageColor module. - #

- # If a mask is given, this method updates only the regions - # indicated by the mask. You can use either "1", "L" or "RGBA" - # images (in the latter case, the alpha band is used as mask). - # Where the mask is 255, the given image is copied as is. Where - # the mask is 0, the current value is preserved. Intermediate - # values can be used for transparency effects. - #

- # Note that if you paste an "RGBA" image, the alpha band is - # ignored. You can work around this by using the same image as - # both source image and mask. - # - # @param im Source image or pixel value (integer or tuple). - # @param box An optional 4-tuple giving the region to paste into. - # If a 2-tuple is used instead, it's treated as the upper left - # corner. If omitted or None, the source is pasted into the - # upper left corner. - #

- # If an image is given as the second argument and there is no - # third, the box defaults to (0, 0), and the second argument - # is interpreted as a mask image. - # @param mask An optional mask image. - # @return An Image object. - - def paste(self, im, box=None, mask=None): - "Paste other image into region" - - if isImageType(box) and mask is None: - # abbreviated paste(im, mask) syntax - mask = box; box = None - - if box is None: - # cover all of self - box = (0, 0) + self.size - - if len(box) == 2: - # lower left corner given; get size from image or mask - if isImageType(im): - size = im.size - elif isImageType(mask): - size = mask.size - else: - # FIXME: use self.size here? - raise ValueError( - "cannot determine region size; use 4-item box" - ) - box = box + (box[0]+size[0], box[1]+size[1]) - - if isStringType(im): - import ImageColor - im = ImageColor.getcolor(im, self.mode) - - elif isImageType(im): - im.load() - if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): - # should use an adapter for this! - im = im.convert(self.mode) - im = im.im - - self.load() - if self.readonly: - self._copy() - - if mask: - mask.load() - self.im.paste(im, box, mask.im) - else: - self.im.paste(im, box) - - ## - # Maps this image through a lookup table or function. - # - # @param lut A lookup table, containing 256 values per band in the - # image. A function can be used instead, it should take a single - # argument. The function is called once for each possible pixel - # value, and the resulting table is applied to all bands of the - # image. - # @param mode Output mode (default is same as input). In the - # current version, this can only be used if the source image - # has mode "L" or "P", and the output has mode "1". - # @return An Image object. - - def point(self, lut, mode=None): - "Map image through lookup table" - - self.load() - - if isinstance(lut, ImagePointHandler): - return lut.point(self) - - if not isSequenceType(lut): - # if it isn't a list, it should be a function - if self.mode in ("I", "I;16", "F"): - # check if the function can be used with point_transform - scale, offset = _getscaleoffset(lut) - return self._new(self.im.point_transform(scale, offset)) - # for other modes, convert the function to a table - lut = map(lut, range(256)) * self.im.bands - - if self.mode == "F": - # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") - - return self._new(self.im.point(lut, mode)) - - ## - # Adds or replaces the alpha layer in this image. If the image - # does not have an alpha layer, it's converted to "LA" or "RGBA". - # The new layer must be either "L" or "1". - # - # @param im The new alpha layer. This can either be an "L" or "1" - # image having the same size as this image, or an integer or - # other color value. - - def putalpha(self, alpha): - "Set alpha layer" - - self.load() - if self.readonly: - self._copy() - - if self.mode not in ("LA", "RGBA"): - # attempt to promote self to a matching alpha mode - try: - mode = getmodebase(self.mode) + "A" - try: - self.im.setmode(mode) - except (AttributeError, ValueError): - # do things the hard way - im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): - raise ValueError # sanity check - self.im = im - self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") - - if self.mode == "LA": - band = 1 - else: - band = 3 - - if isImageType(alpha): - # alpha layer - if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") - alpha.load() - if alpha.mode == "1": - alpha = alpha.convert("L") - else: - # constant alpha - try: - self.im.fillband(band, alpha) - except (AttributeError, ValueError): - # do things the hard way - alpha = new("L", self.size, alpha) - else: - return - - self.im.putband(alpha.im, band) - - ## - # Copies pixel data to this image. This method copies data from a - # sequence object into the image, starting at the upper left - # corner (0, 0), and continuing until either the image or the - # sequence ends. The scale and offset values are used to adjust - # the sequence values: pixel = value*scale + offset. - # - # @param data A sequence object. - # @param scale An optional scale value. The default is 1.0. - # @param offset An optional offset value. The default is 0.0. - - def putdata(self, data, scale=1.0, offset=0.0): - "Put data from a sequence object into an image." - - self.load() - if self.readonly: - self._copy() - - self.im.putdata(data, scale, offset) - - ## - # Attaches a palette to this image. The image must be a "P" or - # "L" image, and the palette sequence must contain 768 integer - # values, where each group of three values represent the red, - # green, and blue values for the corresponding pixel - # index. Instead of an integer sequence, you can use an 8-bit - # string. - # - # @def putpalette(data) - # @param data A palette sequence (either a list or a string). - - def putpalette(self, data, rawmode="RGB"): - "Put palette data into an image." - - if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") - self.load() - if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) - else: - if not isStringType(data): - data = string.join(map(chr, data), "") - palette = ImagePalette.raw(rawmode, data) - self.mode = "P" - self.palette = palette - self.palette.mode = "RGB" - self.load() # install new palette - - ## - # Modifies the pixel at the given position. The colour is given as - # a single numerical value for single-band images, and a tuple for - # multi-band images. - #

- # Note that this method is relatively slow. For more extensive - # changes, use {@link #Image.paste} or the ImageDraw module - # instead. - # - # @param xy The pixel coordinate, given as (x, y). - # @param value The pixel value. - # @see #Image.paste - # @see #Image.putdata - # @see ImageDraw - - def putpixel(self, xy, value): - "Set pixel value" - - self.load() - if self.readonly: - self._copy() - - return self.im.putpixel(xy, value) - - ## - # Returns a resized copy of this image. - # - # @def resize(size, filter=NEAREST) - # @param size The requested size in pixels, as a 2-tuple: - # (width, height). - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), BICUBIC - # (cubic spline interpolation in a 4x4 environment), or - # ANTIALIAS (a high-quality downsampling filter). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @return An Image object. - - def resize(self, size, resample=NEAREST): - "Resize image" - - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) - - ## - # Returns a rotated copy of this image. This method returns a - # copy of this image, rotated the given number of degrees counter - # clockwise around its centre. - # - # @def rotate(angle, filter=NEAREST) - # @param angle In degrees counter clockwise. - # @param filter An optional resampling filter. This can be - # one of NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or BICUBIC - # (cubic spline interpolation in a 4x4 environment). - # If omitted, or if the image has mode "1" or "P", it is - # set NEAREST. - # @param expand Optional expansion flag. If true, expands the output - # image to make it large enough to hold the entire rotated image. - # If false or omitted, make the output image the same size as the - # input image. - # @return An Image object. - - def rotate(self, angle, resample=NEAREST, expand=0): - "Rotate image. Angle given as degrees counter-clockwise." - - if expand: - import math - angle = -angle * math.pi / 180 - matrix = [ - math.cos(angle), math.sin(angle), 0.0, - -math.sin(angle), math.cos(angle), 0.0 - ] - def transform(x, y, (a, b, c, d, e, f)=matrix): - return a*x + b*y + c, d*x + e*y + f - - # calculate output size - w, h = self.size - xx = [] - yy = [] - for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) - xx.append(x) - yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) - - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y - - return self.transform((w, h), AFFINE, matrix, resample) - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - self.load() - - if self.mode in ("1", "P"): - resample = NEAREST - - return self._new(self.im.rotate(angle, resample)) - - ## - # Saves this image under the given filename. If no format is - # specified, the format to use is determined from the filename - # extension, if possible. - #

- # Keyword options can be used to provide additional instructions - # to the writer. If a writer doesn't recognise an option, it is - # silently ignored. The available options are described later in - # this handbook. - #

- # You can use a file object instead of a filename. In this case, - # you must always specify the format. The file object must - # implement the seek, tell, and write - # methods, and be opened in binary mode. - # - # @def save(file, format=None, **options) - # @param file File name or file object. - # @param format Optional format override. If omitted, the - # format to use is determined from the filename extension. - # If a file object was used instead of a filename, this - # parameter should always be used. - # @param **options Extra parameters to the image writer. - # @return None - # @exception KeyError If the output format could not be determined - # from the file name. Use the format option to solve this. - # @exception IOError If the file could not be written. The file - # may have been created, and may contain partial data. - - def save(self, fp, format=None, **params): - "Save image to file or stream" - - if isStringType(fp): - filename = fp - else: - if hasattr(fp, "name") and isStringType(fp.name): - filename = fp.name - else: - filename = "" - - # may mutate self! - self.load() - - self.encoderinfo = params - self.encoderconfig = () - - preinit() - - ext = string.lower(os.path.splitext(filename)[1]) - - if not format: - try: - format = EXTENSION[ext] - except KeyError: - init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension - - try: - save_handler = SAVE[string.upper(format)] - except KeyError: - init() - save_handler = SAVE[string.upper(format)] # unknown format - - if isStringType(fp): - import __builtin__ - fp = __builtin__.open(fp, "wb") - close = 1 - else: - close = 0 - - try: - save_handler(self, fp, filename) - finally: - # do what we can to clean up - if close: - fp.close() - - ## - # Seeks to the given frame in this sequence file. If you seek - # beyond the end of the sequence, the method raises an - # EOFError exception. When a sequence file is opened, the - # library automatically seeks to frame 0. - #

- # Note that in the current version of the library, most sequence - # formats only allows you to seek to the next frame. - # - # @param frame Frame number, starting at 0. - # @exception EOFError If the call attempts to seek beyond the end - # of the sequence. - # @see #Image.tell - - def seek(self, frame): - "Seek to given frame in sequence file" - - # overridden by file handlers - if frame != 0: - raise EOFError - - ## - # Displays this image. This method is mainly intended for - # debugging purposes. - #

- # On Unix platforms, this method saves the image to a temporary - # PPM file, and calls the xv utility. - #

- # On Windows, it saves the image to a temporary BMP file, and uses - # the standard BMP display utility to show it (usually Paint). - # - # @def show(title=None) - # @param title Optional title to use for the image window, - # where possible. - - def show(self, title=None, command=None): - "Display image (for debug purposes only)" - - _show(self, title=title, command=command) - - ## - # Split this image into individual bands. This method returns a - # tuple of individual image bands from an image. For example, - # splitting an "RGB" image creates three new images each - # containing a copy of one of the original bands (red, green, - # blue). - # - # @return A tuple containing bands. - - def split(self): - "Split image into bands" - - if self.im.bands == 1: - ims = [self.copy()] - else: - ims = [] - self.load() - for i in range(self.im.bands): - ims.append(self._new(self.im.getband(i))) - return tuple(ims) - - ## - # Returns the current frame number. - # - # @return Frame number, starting with 0. - # @see #Image.seek - - def tell(self): - "Return current frame number" - - return 0 - - ## - # Make this image into a thumbnail. This method modifies the - # image to contain a thumbnail version of itself, no larger than - # the given size. This method calculates an appropriate thumbnail - # size to preserve the aspect of the image, calls the {@link - # #Image.draft} method to configure the file reader (where - # applicable), and finally resizes the image. - #

- # Note that the bilinear and bicubic filters in the current - # version of PIL are not well-suited for thumbnail generation. - # You should use ANTIALIAS unless speed is much more - # important than quality. - #

- # Also note that this function modifies the Image object in place. - # If you need to use the full resolution image as well, apply this - # method to a {@link #Image.copy} of the original image. - # - # @param size Requested size. - # @param resample Optional resampling filter. This can be one - # of NEAREST, BILINEAR, BICUBIC, or - # ANTIALIAS (best quality). If omitted, it defaults - # to NEAREST (this will be changed to ANTIALIAS in a - # future version). - # @return None - - def thumbnail(self, size, resample=NEAREST): - "Create thumbnail representation (modifies image in place)" - - # FIXME: the default resampling filter will be changed - # to ANTIALIAS in future versions - - # preserve aspect ratio - x, y = self.size - if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] - if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] - size = x, y - - if size == self.size: - return - - self.draft(None, size) - - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback - - self.im = im.im - self.mode = im.mode - self.size = size - - self.readonly = 0 - - # FIXME: the different tranform methods need further explanation - # instead of bloating the method docs, add a separate chapter. - - ## - # Transforms this image. This method creates a new image with the - # given size, and the same mode as the original, and copies data - # to the new image using the given transform. - #

- # @def transform(size, method, data, resample=NEAREST) - # @param size The output size. - # @param method The transformation method. This is one of - # EXTENT (cut out a rectangular subregion), AFFINE - # (affine transform), PERSPECTIVE (perspective - # transform), QUAD (map a quadrilateral to a - # rectangle), or MESH (map a number of source quadrilaterals - # in one operation). - # @param data Extra data to the transformation method. - # @param resample Optional resampling filter. It can be one of - # NEAREST (use nearest neighbour), BILINEAR - # (linear interpolation in a 2x2 environment), or - # BICUBIC (cubic spline interpolation in a 4x4 - # environment). If omitted, or if the image has mode - # "1" or "P", it is set to NEAREST. - # @return An Image object. - - def transform(self, size, method, data=None, resample=NEAREST, fill=1): - "Transform image" - - if isinstance(method, ImageTransformHandler): - return method.transform(size, self, resample=resample, fill=fill) - if hasattr(method, "getdata"): - # compatibility w. old-style transform objects - method, data = method.getdata() - if data is None: - raise ValueError("missing method data") - im = new(self.mode, size, None) - if method == MESH: - # list of quads - for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fill) - else: - im.__transformer((0, 0)+size, self, method, data, resample, fill) - - return im - - def __transformer(self, box, image, method, data, - resample=NEAREST, fill=1): - - # FIXME: this should be turned into a lazy operation (?) - - w = box[2]-box[0] - h = box[3]-box[1] - - if method == AFFINE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4]) - elif method == EXTENT: - # convert extent to an affine transform - x0, y0, x1, y1 = data - xs = float(x1 - x0) / w - ys = float(y1 - y0) / h - method = AFFINE - data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) - elif method == PERSPECTIVE: - # change argument order to match implementation - data = (data[2], data[0], data[1], - data[5], data[3], data[4], - data[6], data[7]) - elif method == QUAD: - # quadrilateral warp. data specifies the four corners - # given as NW, SW, SE, and NE. - nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] - x0, y0 = nw; As = 1.0 / w; At = 1.0 / h - data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, - (se[0]-sw[0]-ne[0]+x0)*As*At, - y0, (ne[1]-y0)*As, (sw[1]-y0)*At, - (se[1]-sw[1]-ne[1]+y0)*As*At) - else: - raise ValueError("unknown transformation method") - - if resample not in (NEAREST, BILINEAR, BICUBIC): - raise ValueError("unknown resampling filter") - - image.load() - - self.load() - - if image.mode in ("1", "P"): - resample = NEAREST - - self.im.transform2(box, image.im, method, data, resample, fill) - - ## - # Returns a flipped or rotated copy of this image. - # - # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, - # ROTATE_90, ROTATE_180, or ROTATE_270. - - def transpose(self, method): - "Transpose image (flip or rotate in 90 degree steps)" - - self.load() - im = self.im.transpose(method) - return self._new(im) - -# -------------------------------------------------------------------- -# Lazy operations - -class _ImageCrop(Image): - - def __init__(self, im, box): - - Image.__init__(self) - - x0, y0, x1, y1 = box - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 - - self.mode = im.mode - self.size = x1-x0, y1-y0 - - self.__crop = x0, y0, x1, y1 - - self.im = im.im - - def load(self): - - # lazy evaluation! - if self.__crop: - self.im = self.im.crop(self.__crop) - self.__crop = None - - if self.im: - return self.im.pixel_access(self.readonly) - - # FIXME: future versions should optimize crop/paste - # sequences! - -# -------------------------------------------------------------------- -# Abstract handlers. - -class ImagePointHandler: - # used as a mixin by point transforms (for use with im.point) - pass - -class ImageTransformHandler: - # used as a mixin by geometry transforms (for use with im.transform) - pass - -# -------------------------------------------------------------------- -# Factories - -# -# Debugging - -def _wedge(): - "Create greyscale wedge (for debugging only)" - - return Image()._new(core.wedge("L")) - -## -# Creates a new image with the given mode and size. -# -# @param mode The mode to use for the new image. -# @param size A 2-tuple, containing (width, height) in pixels. -# @param color What colour to use for the image. Default is black. -# If given, this should be a single integer or floating point value -# for single-band modes, and a tuple for multi-band modes (one value -# per band). When creating RGB images, you can also use colour -# strings as supported by the ImageColor module. If the colour is -# None, the image is not initialised. -# @return An Image object. - -def new(mode, size, color=0): - "Create a new image" - - if color is None: - # don't initialize - return Image()._new(core.new(mode, size)) - - if isStringType(color): - # css3-style specifier - - import ImageColor - color = ImageColor.getcolor(color, mode) - - return Image()._new(core.fill(mode, size, color)) - -## -# Creates an image memory from pixel data in a string. -#

-# In its simplest form, this function takes three arguments -# (mode, size, and unpacked pixel data). -#

-# You can also use any pixel decoder supported by PIL. For more -# information on available decoders, see the section Writing Your Own File Decoder. -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string containing raw data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. -# @return An Image object. - -def fromstring(mode, size, data, decoder_name="raw", *args): - "Load image from string" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw" and args == (): - args = mode - - im = new(mode, size) - im.fromstring(data, decoder_name, args) - return im - -## -# (New in 1.1.4) Creates an image memory from pixel data in a string -# or byte buffer. -#

-# This function is similar to {@link #fromstring}, but uses data in -# the byte buffer, where possible. This means that changes to the -# original buffer object are reflected in this image). Not all modes -# can share memory; supported modes include "L", "RGBX", "RGBA", and -# "CMYK". -#

-# Note that this function decodes pixel data only, not entire images. -# If you have an entire image file in a string, wrap it in a -# StringIO object, and use {@link #open} to load it. -#

-# In the current version, the default parameters used for the "raw" -# decoder differs from that used for {@link fromstring}. This is a -# bug, and will probably be fixed in a future release. The current -# release issues a warning if you do this; to disable the warning, -# you should provide the full set of parameters. See below for -# details. -# -# @param mode The image mode. -# @param size The image size. -# @param data An 8-bit string or other buffer object containing raw -# data for the given mode. -# @param decoder_name What decoder to use. -# @param *args Additional parameters for the given decoder. For the -# default encoder ("raw"), it's recommended that you provide the -# full set of parameters: -# frombuffer(mode, size, data, "raw", mode, 0, 1). -# @return An Image object. -# @since 1.1.4 - -def frombuffer(mode, size, data, decoder_name="raw", *args): - "Load image from string or buffer" - - # may pass tuple instead of argument list - if len(args) == 1 and isTupleType(args[0]): - args = args[0] - - if decoder_name == "raw": - if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 - if args[0] in _MAPMODES: - im = new(mode, (1,1)) - im = im._new( - core.map_buffer(data, size, decoder_name, None, 0, args) - ) - im.readonly = 1 - return im - - return fromstring(mode, size, data, decoder_name, args) - - -## -# (New in 1.1.6) Creates an image memory from an object exporting -# the array interface (using the buffer protocol). -# -# If obj is not contiguous, then the tostring method is called -# and {@link frombuffer} is used. -# -# @param obj Object with array interface -# @param mode Mode to use (will be determined from type if None) -# @return An image memory. - -def fromarray(obj, mode=None): - arr = obj.__array_interface__ - shape = arr['shape'] - ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr['typestr'] - mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - # print typekey - raise TypeError("Cannot handle this data type") - else: - rawmode = mode - if mode in ["1", "L", "I", "P", "F"]: - ndmax = 2 - elif mode == "RGB": - ndmax = 3 - else: - ndmax = 4 - if ndim > ndmax: - raise ValueError("Too many dimensions.") - - size = shape[1], shape[0] - if strides is not None: - obj = obj.tostring() - - return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) - -_fromarray_typemap = { - # (shape, typestr) => mode, rawmode - # first two members of shape are set to one - # ((1, 1), "|b1"): ("1", "1"), # broken - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "i2"): ("I", "I;16B"), - ((1, 1), "i4"): ("I", "I;32B"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), - } - -# shortcuts -_fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") -_fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F") - -## -# Opens and identifies the given image file. -#

-# This is a lazy operation; this function identifies the file, but the -# actual image data is not read from the file until you try to process -# the data (or call the {@link #Image.load} method). -# -# @def open(file, mode="r") -# @param file A filename (string) or a file object. The file object -# must implement read, seek, and tell methods, -# and be opened in binary mode. -# @param mode The mode. If given, this argument must be "r". -# @return An Image object. -# @exception IOError If the file cannot be found, or the image cannot be -# opened and identified. -# @see #new - -def open(fp, mode="r"): - "Open an image file, without loading the raster data" - - if mode != "r": - raise ValueError("bad mode") - - if isStringType(fp): - import __builtin__ - filename = fp - fp = __builtin__.open(fp, "rb") - else: - filename = "" - - prefix = fp.read(16) - - preinit() - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - if init(): - - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - return factory(fp, filename) - except (SyntaxError, IndexError, TypeError): - pass - - raise IOError("cannot identify image file") - -# -# Image processing. - -## -# Creates a new image by interpolating between two input images, using -# a constant alpha. -# -#

-#    out = image1 * (1.0 - alpha) + image2 * alpha
-# 
-# -# @param im1 The first image. -# @param im2 The second image. Must have the same mode and size as -# the first image. -# @param alpha The interpolation alpha factor. If alpha is 0.0, a -# copy of the first image is returned. If alpha is 1.0, a copy of -# the second image is returned. There are no restrictions on the -# alpha value. If necessary, the result is clipped to fit into -# the allowed output range. -# @return An Image object. - -def blend(im1, im2, alpha): - "Interpolate between images." - - im1.load() - im2.load() - return im1._new(core.blend(im1.im, im2.im, alpha)) - -## -# Creates a new image by interpolating between two input images, -# using the mask as alpha. -# -# @param image1 The first image. -# @param image2 The second image. Must have the same mode and -# size as the first image. -# @param mask A mask image. This image can can have mode -# "1", "L", or "RGBA", and must have the same size as the -# other two images. - -def composite(image1, image2, mask): - "Create composite image by blending images using a transparency mask" - - image = image2.copy() - image.paste(image1, None, mask) - return image - -## -# Applies the function (which should take one argument) to each pixel -# in the given image. If the image has more than one band, the same -# function is applied to each band. Note that the function is -# evaluated once for each possible pixel value, so you cannot use -# random components or other generators. -# -# @def eval(image, function) -# @param image The input image. -# @param function A function object, taking one integer argument. -# @return An Image object. - -def eval(image, *args): - "Evaluate image expression" - - return image.point(args[0]) - -## -# Creates a new image from a number of single-band images. -# -# @param mode The mode to use for the output image. -# @param bands A sequence containing one single-band image for -# each band in the output image. All bands must have the -# same size. -# @return An Image object. - -def merge(mode, bands): - "Merge a set of single band images into a new multiband image." - - if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") - for im in bands[1:]: - if im.mode != getmodetype(mode): - raise ValueError("mode mismatch") - if im.size != bands[0].size: - raise ValueError("size mismatch") - im = core.new(mode, bands[0].size) - for i in range(getmodebands(mode)): - bands[i].load() - im.putband(bands[i].im, i) - return bands[0]._new(im) - -# -------------------------------------------------------------------- -# Plugin registry - -## -# Register an image file plugin. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param factory An image file factory method. -# @param accept An optional function that can be used to quickly -# reject images having another format. - -def register_open(id, factory, accept=None): - id = string.upper(id) - ID.append(id) - OPEN[id] = factory, accept - -## -# Registers an image MIME type. This function should not be used -# in application code. -# -# @param id An image format identifier. -# @param mimetype The image MIME type for this format. - -def register_mime(id, mimetype): - MIME[string.upper(id)] = mimetype - -## -# Registers an image save function. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param driver A function to save images in this format. - -def register_save(id, driver): - SAVE[string.upper(id)] = driver - -## -# Registers an image extension. This function should not be -# used in application code. -# -# @param id An image format identifier. -# @param extension An extension used for this format. - -def register_extension(id, extension): - EXTENSION[string.lower(extension)] = string.upper(id) - - -# -------------------------------------------------------------------- -# Simple display support. User code may override this. - -def _show(image, **options): - # override me, as necessary - apply(_showxv, (image,), options) - -def _showxv(image, title=None, **options): - import ImageShow - apply(ImageShow.show, (image, title), options) diff --git a/pyqtgraph/PIL_Fix/README b/pyqtgraph/PIL_Fix/README deleted file mode 100644 index 3711e11310..0000000000 --- a/pyqtgraph/PIL_Fix/README +++ /dev/null @@ -1,11 +0,0 @@ -The file Image.py is a drop-in replacement for the same file in PIL 1.1.6. -It adds support for reading 16-bit TIFF files and converting then to numpy arrays. -(I submitted the changes to the PIL folks long ago, but to my knowledge the code -is not being used by them.) - -To use, copy this file into - /usr/lib/python2.6/dist-packages/PIL/ -or - C:\Python26\lib\site-packages\PIL\ - -..or wherever your system keeps its python modules. diff --git a/pyqtgraph/util/pil_fix.py b/pyqtgraph/util/pil_fix.py new file mode 100644 index 0000000000..da1c52b36c --- /dev/null +++ b/pyqtgraph/util/pil_fix.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Importing this module installs support for 16-bit images in PIL. +This works by patching objects in the PIL namespace; no files are +modified. +""" + +from PIL import Image + +if Image.VERSION == '1.1.7': + Image._MODE_CONV["I;16"] = ('%su2' % Image._ENDIAN, None) + Image._fromarray_typemap[((1, 1), " ndmax: + raise ValueError("Too many dimensions.") + + size = shape[:2][::-1] + if strides is not None: + obj = obj.tostring() + + return frombuffer(mode, size, obj, "raw", mode, 0, 1) + + Image.fromarray=fromarray \ No newline at end of file From 4896de5ee49715da47f7719bc68b5da9b8360bc9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Aug 2014 12:45:35 -0400 Subject: [PATCH 05/33] Fixed item context menus appearing after mouse has exited the item area. This occurred because the scene does not receive mouse move events while a context menu is displayed. If the user right-clicks on a new location while the menu is open, then the click event is delieverd as if the mouse had not moved. Corrected by sending a just-in-time hover event immediately before mouse press, if the cursor has moved. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 37 ++++++++++-------------- pyqtgraph/GraphicsScene/mouseEvents.py | 3 ++ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index a57cca345a..c6afbe0f55 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -135,8 +135,13 @@ def setMoveDistance(self, d): def mousePressEvent(self, ev): #print 'scenePress' QtGui.QGraphicsScene.mousePressEvent(self, ev) - #print "mouseGrabberItem: ", self.mouseGrabberItem() if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + if self.lastHoverEvent is not None: + # If the mouse has moved since the last hover event, send a new one. + # This can happen if a context menu is open while the mouse is moving. + if ev.scenePos() != self.lastHoverEvent.scenePos(): + self.sendHoverEvents(ev) + self.clickEvents.append(MouseClickEvent(ev)) ## set focus on the topmost focusable item under this click @@ -145,10 +150,6 @@ def mousePressEvent(self, ev): if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0: i.setFocus(QtCore.Qt.MouseFocusReason) break - #else: - #addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem)) - #item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem()) - #print "click grabbed by:", item def mouseMoveEvent(self, ev): self.sigMouseMoved.emit(ev.scenePos()) @@ -189,7 +190,6 @@ def leaveEvent(self, ev): ## inform items that mouse is gone def mouseReleaseEvent(self, ev): #print 'sceneRelease' if self.mouseGrabberItem() is None: - #print "sending click/drag event" if ev.button() in self.dragButtons: if self.sendDragEvent(ev, final=True): #print "sent drag event" @@ -231,6 +231,8 @@ def sendHoverEvents(self, ev, exitOnly=False): prevItems = list(self.hoverItems.keys()) + #print "hover prev items:", prevItems + #print "hover test items:", items for item in items: if hasattr(item, 'hoverEvent'): event.currentItem = item @@ -248,6 +250,7 @@ def sendHoverEvents(self, ev, exitOnly=False): event.enter = False event.exit = True + #print "hover exit items:", prevItems for item in prevItems: event.currentItem = item try: @@ -257,9 +260,13 @@ def sendHoverEvents(self, ev, exitOnly=False): finally: del self.hoverItems[item] - if hasattr(ev, 'buttons') and int(ev.buttons()) == 0: + # Update last hover event unless: + # - mouse is dragging (move+buttons); in this case we want the dragged + # item to continue receiving events until the drag is over + # - event is not a mouse event (QEvent.Leave sometimes appears here) + if (ev.type() == ev.GraphicsSceneMousePress or + (ev.type() == ev.GraphicsSceneMouseMove and int(ev.buttons()) == 0)): self.lastHoverEvent = event ## save this so we can ask about accepted events later. - def sendDragEvent(self, ev, init=False, final=False): ## Send a MouseDragEvent to the current dragItem or to @@ -323,7 +330,6 @@ def sendClickEvent(self, ev): acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None) else: acceptedItem = None - if acceptedItem is not None: ev.currentItem = acceptedItem try: @@ -345,22 +351,9 @@ def sendClickEvent(self, ev): if int(item.flags() & item.ItemIsFocusable) > 0: item.setFocus(QtCore.Qt.MouseFocusReason) break - #if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton: - #print "GraphicsScene emitting sigSceneContextMenu" - #self.sigMouseClicked.emit(ev) - #ev.accept() self.sigMouseClicked.emit(ev) return ev.isAccepted() - #def claimEvent(self, item, button, eventType): - #key = (button, eventType) - #if key in self.claimedEvents: - #return False - #self.claimedEvents[key] = item - #print "event", key, "claimed by", item - #return True - - def items(self, *args): #print 'args:', args items = QtGui.QGraphicsScene.items(self, *args) diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index 7809d464f4..2e472e04d1 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -355,6 +355,9 @@ def lastPos(self): return Point(self.currentItem.mapFromScene(self._lastScenePos)) def __repr__(self): + if self.exit: + return "" + if self.currentItem is None: lp = self._lastScenePos p = self._scenePos From 490148fe5c5091b1b2bc961356188fc5cc8c3187 Mon Sep 17 00:00:00 2001 From: Jacob Welsh Date: Sun, 24 Aug 2014 17:44:39 -0500 Subject: [PATCH 06/33] Fix getGitVersion showing a clean repo as modified --- tools/setupHelpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index b23fea0a4d..b308b2261b 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -377,9 +377,9 @@ def getGitVersion(tagPrefix): # any uncommitted modifications? modified = False - status = check_output(['git', 'status', '-s'], universal_newlines=True).strip().split('\n') + status = check_output(['git', 'status', '--porcelain'], universal_newlines=True).strip().split('\n') for line in status: - if line[:2] != '??': + if line != '' and line[:2] != '??': modified = True break @@ -558,5 +558,3 @@ def initialize_options(self): def finalize_options(self): pass - - \ No newline at end of file From a7b0bbb3bb0f025e94c12e1d918862b732550ffc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Aug 2014 22:50:14 -0400 Subject: [PATCH 07/33] Fixed AxisItem ignoring setWidth when label is displayed --- pyqtgraph/graphicsItems/AxisItem.py | 100 ++++++++++++++++++---------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e5b9e3f5ae..b125cb7e5b 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -62,6 +62,11 @@ def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLen self.textWidth = 30 ## Keeps track of maximum width / height of tick text self.textHeight = 18 + # If the user specifies a width / height, remember that setting + # indefinitely. + self.fixedWidth = None + self.fixedHeight = None + self.labelText = '' self.labelUnits = '' self.labelUnitPrefix='' @@ -219,9 +224,9 @@ def showLabel(self, show=True): #self.drawLabel = show self.label.setVisible(show) if self.orientation in ['left', 'right']: - self.setWidth() + self._updateWidth() else: - self.setHeight() + self._updateHeight() if self.autoSIPrefix: self.updateAutoSIPrefix() @@ -291,54 +296,80 @@ def _updateMaxTextSize(self, x): if mx > self.textWidth or mx < self.textWidth-10: self.textWidth = mx if self.style['autoExpandTextSpace'] is True: - self.setWidth() + self._updateWidth() #return True ## size has changed else: mx = max(self.textHeight, x) if mx > self.textHeight or mx < self.textHeight-10: self.textHeight = mx if self.style['autoExpandTextSpace'] is True: - self.setHeight() + self._updateHeight() #return True ## size has changed def _adjustSize(self): if self.orientation in ['left', 'right']: - self.setWidth() + self._updateWidth() else: - self.setHeight() + self._updateHeight() def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. - The height of the axis label is automatically added.""" - if h is None: - if not self.style['showValues']: - h = 0 - elif self.style['autoExpandTextSpace'] is True: - h = self.textHeight + The height of the axis label is automatically added. + + If *height* is None, then the value will be determined automatically + based on the size of the tick text.""" + self.fixedHeight = h + self._updateHeight() + + def _updateHeight(self): + if not self.isVisible(): + h = 0 + else: + if self.fixedHeight is None: + if not self.style['showValues']: + h = 0 + elif self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] + h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0 + h += max(0, self.style['tickLength']) + if self.label.isVisible(): + h += self.label.boundingRect().height() * 0.8 else: - h = self.style['tickTextHeight'] - h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0 - h += max(0, self.style['tickLength']) - if self.label.isVisible(): - h += self.label.boundingRect().height() * 0.8 + h = self.fixedHeight + self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. - The width of the axis label is automatically added.""" - if w is None: - if not self.style['showValues']: - w = 0 - elif self.style['autoExpandTextSpace'] is True: - w = self.textWidth + The width of the axis label is automatically added. + + If *width* is None, then the value will be determined automatically + based on the size of the tick text.""" + self.fixedWidth = w + self._updateWidth() + + def _updateWidth(self): + if not self.isVisible(): + w = 0 + else: + if self.fixedWidth is None: + if not self.style['showValues']: + w = 0 + elif self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] + w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0 + w += max(0, self.style['tickLength']) + if self.label.isVisible(): + w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate else: - w = self.style['tickTextWidth'] - w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0 - w += max(0, self.style['tickLength']) - if self.label.isVisible(): - w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate + w = self.fixedWidth + self.setMaximumWidth(w) self.setMinimumWidth(w) self.picture = None @@ -1009,19 +1040,18 @@ def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): profiler('draw text') def show(self): - + GraphicsWidget.show(self) if self.orientation in ['left', 'right']: - self.setWidth() + self._updateWidth() else: - self.setHeight() - GraphicsWidget.show(self) + self._updateHeight() def hide(self): + GraphicsWidget.hide(self) if self.orientation in ['left', 'right']: - self.setWidth(0) + self._updateWidth() else: - self.setHeight(0) - GraphicsWidget.hide(self) + self._updateHeight() def wheelEvent(self, ev): if self.linkedView() is None: From 70d9f1eeed1f141cdf1b8e8fbc029cdd8a0e9fef Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Sun, 28 Sep 2014 08:26:13 -0700 Subject: [PATCH 08/33] Fix OpenGL shader/texture sharing on PySide --- pyqtgraph/opengl/GLViewWidget.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index c71bb3c92c..788ab72530 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -7,6 +7,8 @@ ##Vector = QtGui.QVector3D +ShareWidget = None + class GLViewWidget(QtOpenGL.QGLWidget): """ Basic widget for displaying 3D data @@ -16,14 +18,14 @@ class GLViewWidget(QtOpenGL.QGLWidget): """ - ShareWidget = None - def __init__(self, parent=None): - if GLViewWidget.ShareWidget is None: + global ShareWidget + + if ShareWidget is None: ## create a dummy widget to allow sharing objects (textures, shaders, etc) between views - GLViewWidget.ShareWidget = QtOpenGL.QGLWidget() + ShareWidget = QtOpenGL.QGLWidget() - QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget) + QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget) self.setFocusPolicy(QtCore.Qt.ClickFocus) From 35cacc78aaf4100a8da3fdd9020d7e563fdeab71 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 30 Sep 2014 16:23:00 -0400 Subject: [PATCH 09/33] Update docstrings for TextItem --- pyqtgraph/graphicsItems/TextItem.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 22b1eee62c..d3c9800686 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -44,6 +44,11 @@ def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport def setText(self, text, color=(200,200,200)): + """ + Set the text and color of this item. + + This method sets the plain text of the item; see also setHtml(). + """ color = fn.mkColor(color) self.textItem.setDefaultTextColor(color) self.textItem.setPlainText(text) @@ -57,18 +62,41 @@ def updateAnchor(self): #self.translate(0, 20) def setPlainText(self, *args): + """ + Set the plain text to be rendered by this item. + + See QtGui.QGraphicsTextItem.setPlainText(). + """ self.textItem.setPlainText(*args) self.updateText() def setHtml(self, *args): + """ + Set the HTML code to be rendered by this item. + + See QtGui.QGraphicsTextItem.setHtml(). + """ self.textItem.setHtml(*args) self.updateText() def setTextWidth(self, *args): + """ + Set the width of the text. + + If the text requires more space than the width limit, then it will be + wrapped into multiple lines. + + See QtGui.QGraphicsTextItem.setTextWidth(). + """ self.textItem.setTextWidth(*args) self.updateText() def setFont(self, *args): + """ + Set the font for this text. + + See QtGui.QGraphicsTextItem.setFont(). + """ self.textItem.setFont(*args) self.updateText() From 88bf8880e16df7a7718bcb6e7eff331979d9d4f6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 3 Oct 2014 10:33:08 -0400 Subject: [PATCH 10/33] Correction to exporting docs --- doc/source/exporting.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst index 137e658490..ccd017d707 100644 --- a/doc/source/exporting.rst +++ b/doc/source/exporting.rst @@ -39,13 +39,14 @@ Exporting from the API To export a file programatically, follow this example:: import pyqtgraph as pg + import pyqtgraph.exporters # generate something to export plt = pg.plot([1,5,2,4,3]) # create an exporter instance, as an argument give it # the item you wish to export - exporter = pg.exporters.ImageExporter.ImageExporter(plt.plotItem) + exporter = pg.exporters.ImageExporter(plt.plotItem) # set export parameters if needed exporter.parameters()['width'] = 100 # (note this also affects height parameter) From 8f273f53ab7138baa5fa8a7a9bc8bfa145a55000 Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Wed, 15 Oct 2014 06:16:40 -0700 Subject: [PATCH 11/33] Fix memory leak in GLScatterPlotItem Fixes #103. If a ScatterPlotItem was removed from a plot and added again, glGenTetures was called again unneccesarily. Each time it is called, it eats up a little more space. --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 6cfcc6aa87..dc4b298ae1 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -66,7 +66,8 @@ def fn(x,y): #print pData.shape, pData.min(), pData.max() pData = pData.astype(np.ubyte) - self.pointTexture = glGenTextures(1) + if getattr(self, "pointTexture", None) is None: + self.pointTexture = glGenTextures(1) glActiveTexture(GL_TEXTURE0) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.pointTexture) From 6cc0f5e33da62805cf7864aa7a34e3a27e51c157 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Thu, 16 Oct 2014 12:23:32 +0200 Subject: [PATCH 12/33] fixed the Pen None property. - https://groups.google.com/forum/#!topic/pyqtgraph/t6cl1CevlB0 Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index e39b535a45..3eb93ada33 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -241,8 +241,8 @@ def __init__(self, *args, **kargs): 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': getConfigOption('antialias'), 'name': None, - } - + } + self.setPen(fn.mkPen(getConfigOption('foreground')), update=False) self.setBrush(fn.mkBrush(100,100,150), update=False) self.setSymbol('o', update=False) @@ -351,16 +351,12 @@ def addPoints(self, *args, **kargs): newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size - + if 'spots' in kargs: spots = kargs['spots'] for i in range(len(spots)): spot = spots[i] for k in spot: - #if k == 'pen': - #newData[k] = fn.mkPen(spot[k]) - #elif k == 'brush': - #newData[k] = fn.mkBrush(spot[k]) if k == 'pos': pos = spot[k] if isinstance(pos, QtCore.QPointF): @@ -369,10 +365,10 @@ def addPoints(self, *args, **kargs): x,y = pos[0], pos[1] newData[i]['x'] = x newData[i]['y'] = y - elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']: + elif k == 'pen': + newData[i][k] = fn.mkPen(spot[k]) + elif k in ['x', 'y', 'size', 'symbol', 'brush', 'data']: newData[i][k] = spot[k] - #elif k == 'data': - #self.pointData[i] = spot[k] else: raise Exception("Unknown spot parameter: %s" % k) elif 'y' in kargs: @@ -389,10 +385,10 @@ def addPoints(self, *args, **kargs): if k in kargs: setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod(kargs[k], update=False, dataSet=newData, mask=kargs.get('mask', None)) - + if 'data' in kargs: self.setPointData(kargs['data'], dataSet=newData) - + self.prepareGeometryChange() self.informViewBoundsChanged() self.bounds = [None, None] @@ -428,7 +424,7 @@ def setPen(self, *args, **kargs): all spots which do not have a pen explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) - + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] if kargs['mask'] is not None: From 884df4934af6eadaa0065b700853838a32440576 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Fri, 17 Oct 2014 10:57:36 +0200 Subject: [PATCH 13/33] fixed a keyerror when passing a list into setBrush - https://groups.google.com/forum/#!topic/pyqtgraph/xVyCC2f7gVo Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index f1a5201dc9..ebff444275 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -443,7 +443,7 @@ def setBrush(self, *args, **kargs): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): brushes = args[0] - if kargs['mask'] is not None: + if 'mask' in kargs and kargs['mask'] is not None: brushes = brushes[kargs['mask']] if len(brushes) != len(dataSet): raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) From 7356126c3d2b11e7abcd7c0b34f03dbd81d69d51 Mon Sep 17 00:00:00 2001 From: Nicholas Tan Jerome Date: Fri, 17 Oct 2014 11:18:12 +0200 Subject: [PATCH 14/33] added "mask" key check on setPen as well Signed-off-by: Nicholas Tan Jerome --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index ebff444275..584d455e04 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -421,7 +421,7 @@ def setPen(self, *args, **kargs): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] - if kargs['mask'] is not None: + if if 'mask' in kargs and kargs['mask'] is not None: pens = pens[kargs['mask']] if len(pens) != len(dataSet): raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) From 309133042019244b7f3e4baec1c2b4e3a3c4820d Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 21 Oct 2014 14:37:06 -0700 Subject: [PATCH 15/33] Add recursive submenu support for node library. --- pyqtgraph/flowchart/Flowchart.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 878f86ae2e..7b8cda33fb 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -823,16 +823,20 @@ def reloadLibrary(self): self.buildMenu() def buildMenu(self, pos=None): + def buildSubMenu(node, rootMenu, subMenus, pos=None): + for section, node in node.items(): + menu = QtGui.QMenu(section) + rootMenu.addMenu(menu) + if isinstance(node, OrderedDict): + buildSubMenu(node, menu, subMenus, pos=pos) + subMenus.append(menu) + else: + act = rootMenu.addAction(section) + act.nodeType = section + act.pos = pos self.nodeMenu = QtGui.QMenu() - self.subMenus = [] - for section, nodes in self.chart.library.getNodeTree().items(): - menu = QtGui.QMenu(section) - self.nodeMenu.addMenu(menu) - for name in nodes: - act = menu.addAction(name) - act.nodeType = name - act.pos = pos - self.subMenus.append(menu) + self.subMenus = [] + buildSubMenu(library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu From bcfbe9b4ecd07245693a1b44c73b5d831dd71e0d Mon Sep 17 00:00:00 2001 From: John David Reaver Date: Wed, 22 Oct 2014 16:33:40 -0700 Subject: [PATCH 16/33] Fix PySide error when ViewBox signal destroyed Fixes issue #107 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ceca62c810..ec9c20fe9d 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1718,6 +1718,8 @@ def quit(): pass except TypeError: ## view has already been deleted (?) pass + except AttributeError: # PySide has deleted signal + pass def locate(self, item, timeout=3.0, children=False): """ From 6c6ba8454afcd2362292d819888f003fbd78a75b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 25 Oct 2014 13:01:10 -0400 Subject: [PATCH 17/33] Added unit tests --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- .../graphicsItems/tests/ScatterPlotItem.py | 23 ----- pyqtgraph/graphicsItems/tests/ViewBox.py | 95 ------------------- .../tests/test_ScatterPlotItem.py | 54 +++++++++++ 4 files changed, 55 insertions(+), 119 deletions(-) delete mode 100644 pyqtgraph/graphicsItems/tests/ScatterPlotItem.py delete mode 100644 pyqtgraph/graphicsItems/tests/ViewBox.py create mode 100644 pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 7cb4c0deca..d7eb2bfc22 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -431,7 +431,7 @@ def setPen(self, *args, **kargs): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): pens = args[0] - if if 'mask' in kargs and kargs['mask'] is not None: + if 'mask' in kargs and kargs['mask'] is not None: pens = pens[kargs['mask']] if len(pens) != len(dataSet): raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) diff --git a/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py deleted file mode 100644 index ef8271bfe3..0000000000 --- a/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py +++ /dev/null @@ -1,23 +0,0 @@ -import pyqtgraph as pg -import numpy as np -app = pg.mkQApp() -plot = pg.plot() -app.processEvents() - -# set view range equal to its bounding rect. -# This causes plots to look the same regardless of pxMode. -plot.setRange(rect=plot.boundingRect()) - - -def test_modes(): - for i, pxMode in enumerate([True, False]): - for j, useCache in enumerate([True, False]): - s = pg.ScatterPlotItem() - s.opts['useCache'] = useCache - plot.addItem(s) - s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) - s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) - - -if __name__ == '__main__': - test_modes() diff --git a/pyqtgraph/graphicsItems/tests/ViewBox.py b/pyqtgraph/graphicsItems/tests/ViewBox.py deleted file mode 100644 index 91d9b6171b..0000000000 --- a/pyqtgraph/graphicsItems/tests/ViewBox.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -ViewBox test cases: - -* call setRange then resize; requested range must be fully visible -* lockAspect works correctly for arbitrary aspect ratio -* autoRange works correctly with aspect locked -* call setRange with aspect locked, then resize -* AutoRange with all the bells and whistles - * item moves / changes transformation / changes bounds - * pan only - * fractional range - - -""" - -import pyqtgraph as pg -app = pg.mkQApp() - -imgData = pg.np.zeros((10, 10)) -imgData[0] = 3 -imgData[-1] = 3 -imgData[:,0] = 3 -imgData[:,-1] = 3 - -def testLinkWithAspectLock(): - global win, vb - win = pg.GraphicsWindow() - vb = win.addViewBox(name="image view") - vb.setAspectLocked() - vb.enableAutoRange(x=False, y=False) - p1 = win.addPlot(name="plot 1") - p2 = win.addPlot(name="plot 2", row=1, col=0) - win.ci.layout.setRowFixedHeight(1, 150) - win.ci.layout.setColumnFixedWidth(1, 150) - - def viewsMatch(): - r0 = pg.np.array(vb.viewRange()) - r1 = pg.np.array(p1.vb.viewRange()[1]) - r2 = pg.np.array(p2.vb.viewRange()[1]) - match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all() - return match - - p1.setYLink(vb) - p2.setXLink(vb) - print "link views match:", viewsMatch() - win.show() - print "show views match:", viewsMatch() - img = pg.ImageItem(imgData) - vb.addItem(img) - vb.autoRange() - p1.plot(x=imgData.sum(axis=0), y=range(10)) - p2.plot(x=range(10), y=imgData.sum(axis=1)) - print "add items views match:", viewsMatch() - #p1.setAspectLocked() - #grid = pg.GridItem() - #vb.addItem(grid) - pg.QtGui.QApplication.processEvents() - pg.QtGui.QApplication.processEvents() - #win.resize(801, 600) - -def testAspectLock(): - global win, vb - win = pg.GraphicsWindow() - vb = win.addViewBox(name="image view") - vb.setAspectLocked() - img = pg.ImageItem(imgData) - vb.addItem(img) - - -#app.processEvents() -#print "init views match:", viewsMatch() -#p2.setYRange(-300, 300) -#print "setRange views match:", viewsMatch() -#app.processEvents() -#print "setRange views match (after update):", viewsMatch() - -#print "--lock aspect--" -#p1.setAspectLocked(True) -#print "lockAspect views match:", viewsMatch() -#p2.setYRange(-200, 200) -#print "setRange views match:", viewsMatch() -#app.processEvents() -#print "setRange views match (after update):", viewsMatch() - -#win.resize(100, 600) -#app.processEvents() -#vb.setRange(xRange=[-10, 10], padding=0) -#app.processEvents() -#win.resize(600, 100) -#app.processEvents() -#print vb.viewRange() - - -if __name__ == '__main__': - testLinkWithAspectLock() diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py new file mode 100644 index 0000000000..eb5e43c6a7 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -0,0 +1,54 @@ +import pyqtgraph as pg +import numpy as np +app = pg.mkQApp() +plot = pg.plot() +app.processEvents() + +# set view range equal to its bounding rect. +# This causes plots to look the same regardless of pxMode. +plot.setRange(rect=plot.boundingRect()) + + +def test_scatterplotitem(): + for i, pxMode in enumerate([True, False]): + for j, useCache in enumerate([True, False]): + s = pg.ScatterPlotItem() + s.opts['useCache'] = useCache + plot.addItem(s) + s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) + s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) + + # Test uniform spot updates + s.setSize(10) + s.setBrush('r') + s.setPen('g') + s.setSymbol('+') + app.processEvents() + + # Test list spot updates + s.setSize([10] * 6) + s.setBrush([pg.mkBrush('r')] * 6) + s.setPen([pg.mkPen('g')] * 6) + s.setSymbol(['+'] * 6) + s.setPointData([s] * 6) + app.processEvents() + + # Test array spot updates + s.setSize(np.array([10] * 6)) + s.setBrush(np.array([pg.mkBrush('r')] * 6)) + s.setPen(np.array([pg.mkPen('g')] * 6)) + s.setSymbol(np.array(['+'] * 6)) + s.setPointData(np.array([s] * 6)) + app.processEvents() + + # Test per-spot updates + spot = s.points()[0] + spot.setSize(20) + spot.setBrush('b') + spot.setPen('g') + spot.setSymbol('o') + spot.setData(None) + + +if __name__ == '__main__': + test_scatterplotitem() From 2ac343ac37de1355d9834566e409d5013009a3f4 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Mon, 27 Oct 2014 18:06:31 -0700 Subject: [PATCH 18/33] fixed missing namespace. --- pyqtgraph/flowchart/Flowchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 7b8cda33fb..ab5f4a820b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -836,7 +836,7 @@ def buildSubMenu(node, rootMenu, subMenus, pos=None): act.pos = pos self.nodeMenu = QtGui.QMenu() self.subMenus = [] - buildSubMenu(library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) + buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu From 2d78ce6f87b6a314fc57b5bdb08fb7d230795135 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 07:42:17 -0500 Subject: [PATCH 19/33] Fix attributeerror when using spinbox in parametertree --- pyqtgraph/widgets/SpinBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 23516827e6..1d8600c46c 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -239,7 +239,7 @@ def selectNumber(self): Select the numerical portion of the text to allow quick editing by the user. """ le = self.lineEdit() - text = le.text() + text = asUnicode(le.text()) try: index = text.index(' ') except ValueError: From ad10b066529c37f73bace755558908cdda52d35d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 07:46:10 -0500 Subject: [PATCH 20/33] Correction for spinbox auto-selection without suffix --- pyqtgraph/widgets/SpinBox.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 1d8600c46c..4710140511 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -240,11 +240,14 @@ def selectNumber(self): """ le = self.lineEdit() text = asUnicode(le.text()) - try: - index = text.index(' ') - except ValueError: - return - le.setSelection(0, index) + if self.opts['suffix'] == '': + le.setSelection(0, len(text)) + else: + try: + index = text.index(' ') + except ValueError: + return + le.setSelection(0, index) def value(self): """ From 85d6c86c677998aa10d642e4d505c147a954529c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Nov 2014 08:06:18 -0500 Subject: [PATCH 21/33] Test submenu creation in example --- examples/FlowchartCustomNode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/FlowchartCustomNode.py b/examples/FlowchartCustomNode.py index 54c566224e..1cf1ba102a 100644 --- a/examples/FlowchartCustomNode.py +++ b/examples/FlowchartCustomNode.py @@ -127,7 +127,10 @@ def process(self, dataIn, display=True): ## NodeLibrary: library = fclib.LIBRARY.copy() # start with the default node set library.addNodeType(ImageViewNode, [('Display',)]) -library.addNodeType(UnsharpMaskNode, [('Image',)]) +# Add the unsharp mask node to two locations in the menu to demonstrate +# that we can create arbitrary menu structures +library.addNodeType(UnsharpMaskNode, [('Image',), + ('Submenu_test','submenu2','submenu3')]) fc.setLibrary(library) From 2bf4a0eb7b8ddba8eef0e84668a602a72404c050 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 24 Nov 2014 13:09:59 -0500 Subject: [PATCH 22/33] Workaround for Qt bug: wrap setSpacing and setContentsMargins from internal layout of GraphicsLayout. http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout/27105642#27105642 --- pyqtgraph/graphicsItems/GraphicsLayout.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index b83257360d..6ec38fb577 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -160,4 +160,12 @@ def clear(self): for i in list(self.items.keys()): self.removeItem(i) + def setContentsMargins(self, *args): + # Wrap calls to layout. This should happen automatically, but there + # seems to be a Qt bug: + # http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout + self.layout.setContentsMargins(*args) + def setSpacing(self, *args): + self.layout.setSpacing(*args) + \ No newline at end of file From f6ded808efc89cb65d51edd2257c5a204b856317 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 26 Nov 2014 21:25:17 -0500 Subject: [PATCH 23/33] Fixed a few exit crashes, added unit tests to cover them --- pyqtgraph/GraphicsScene/GraphicsScene.py | 4 +-- pyqtgraph/__init__.py | 19 ++++++++++ pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ pyqtgraph/tests/test_exit_crash.py | 38 ++++++++++++++++++++ pyqtgraph/widgets/GraphicsView.py | 11 ++++-- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 pyqtgraph/tests/test_exit_crash.py diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index c6afbe0f55..6f5354dca4 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -84,8 +84,8 @@ def registerObject(cls, obj): cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - def __init__(self, clickRadius=2, moveDistance=5): - QtGui.QGraphicsScene.__init__(self) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): + QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) self.setMoveDistance(moveDistance) self.exportDirectory = None diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455d7..d539e06be4 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -270,7 +270,12 @@ def renamePyc(startDir): ## Attempts to work around exit crashes: import atexit +_cleanupCalled = False def cleanup(): + global _cleanupCalled + if _cleanupCalled: + return + if not getConfigOption('exitCleanup'): return @@ -295,8 +300,22 @@ def cleanup(): s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object continue + _cleanupCalled = True + atexit.register(cleanup) +# Call cleanup when QApplication quits. This is necessary because sometimes +# the QApplication will quit before the atexit callbacks are invoked. +# Note: cannot connect this function until QApplication has been created, so +# instead we have GraphicsView.__init__ call this for us. +_cleanupConnected = False +def _connectCleanup(): + global _cleanupConnected + if _cleanupConnected: + return + QtGui.QApplication.instance().aboutToQuit.connect(cleanup) + _cleanupConnected = True + ## Optional function for exiting immediately (with some manual teardown) def exit(): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6a915902a9..89ebef3e07 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -49,7 +49,7 @@ def __init__(self, image=None, fillHistogram=True): self.setLayout(self.layout) self.layout.setContentsMargins(1,1,1,1) self.layout.setSpacing(0) - self.vb = ViewBox() + self.vb = ViewBox(parent=self) self.vb.setMaximumWidth(152) self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) @@ -59,7 +59,7 @@ def __init__(self, image=None, fillHistogram=True): self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region.setZValue(1000) self.vb.addItem(self.region) - self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f8959e22d5..4f10b0e388 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -145,7 +145,7 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None self.layout.setVerticalSpacing(0) if viewBox is None: - viewBox = ViewBox() + viewBox = ViewBox(parent=self) self.vb = viewBox self.vb.sigStateChanged.connect(self.viewStateChanged) self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus @@ -168,14 +168,14 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None axisItems = {} self.axes = {} for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, AxisItem(orientation=k)) + axis = axisItems.get(k, AxisItem(orientation=k, parent=self)) axis.linkToView(self.vb) self.axes[k] = {'item': axis, 'pos': pos} self.layout.addItem(axis, *pos) axis.setZValue(-1000) axis.setFlag(axis.ItemNegativeZStacksBehindParent) - self.titleLabel = LabelItem('', size='11pt') + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ec9c20fe9d..900c20386b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1696,6 +1696,8 @@ def updateAllViewLists(): def forgetView(vid, name): if ViewBox is None: ## can happen as python is shutting down return + if QtGui.QApplication.instance() is None: + return ## Called with ID and name of view (the view itself is no longer available) for v in list(ViewBox.AllViews.keys()): if id(v) == vid: diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py new file mode 100644 index 0000000000..69181f2167 --- /dev/null +++ b/pyqtgraph/tests/test_exit_crash.py @@ -0,0 +1,38 @@ +import os, sys, subprocess, tempfile +import pyqtgraph as pg + + +code = """ +import sys +sys.path.insert(0, '{path}') +import pyqtgraph as pg +app = pg.mkQApp() +w = pg.{classname}({args}) +""" + + +def test_exit_crash(): + # For each Widget subclass, run a simple python script that creates an + # instance and then shuts down. The intent is to check for segmentation + # faults when each script exits. + tmp = tempfile.mktemp(".py") + path = os.path.dirname(pg.__file__) + + initArgs = { + 'CheckTable': "[]", + 'ProgressDialog': '"msg"', + 'VerticalLabel': '"msg"', + } + + for name in dir(pg): + obj = getattr(pg, name) + if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget): + continue + + print name + argstr = initArgs.get(name, "") + open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) + proc = subprocess.Popen([sys.executable, tmp]) + assert proc.wait() == 0 + + os.remove(tmp) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 3273ac603a..4062be94fa 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -71,6 +71,13 @@ def __init__(self, parent=None, useOpenGL=None, background='default'): QtGui.QGraphicsView.__init__(self, parent) + # This connects a cleanup function to QApplication.aboutToQuit. It is + # called from here because we have no good way to react when the + # QApplication is created by the user. + # See pyqtgraph.__init__.py + from .. import _connectCleanup + _connectCleanup() + if useOpenGL is None: useOpenGL = getConfigOption('useOpenGL') @@ -102,7 +109,8 @@ def __init__(self, parent=None, useOpenGL=None, background='default'): self.currentItem = None self.clearMouse() self.updateMatrix() - self.sceneObj = GraphicsScene() + # GraphicsScene must have parent or expect crashes! + self.sceneObj = GraphicsScene(parent=self) self.setScene(self.sceneObj) ## Workaround for PySide crash @@ -143,7 +151,6 @@ def setBackground(self, background): def paintEvent(self, ev): self.scene().prepareForPaint() - #print "GV: paint", ev.rect() return QtGui.QGraphicsView.paintEvent(self, ev) def render(self, *args, **kwds): From f90565442c517c4251ee72320b355644c682fa31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Dec 2014 16:39:41 -0500 Subject: [PATCH 24/33] Correction in setup.py: do not raise exception if install location does not exist yet. --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ea5609597b..4b5cab926e 100644 --- a/setup.py +++ b/setup.py @@ -101,11 +101,12 @@ class Install(distutils.command.install.install): """ def run(self): name = self.config_vars['dist_name'] - if name in os.listdir(self.install_libbase): + path = self.install_libbase + if os.path.exists(path) and name in os.listdir(path): raise Exception("It appears another version of %s is already " "installed at %s; remove this before installing." - % (name, self.install_libbase)) - print("Installing to %s" % self.install_libbase) + % (name, path)) + print("Installing to %s" % path) return distutils.command.install.install.run(self) setup( From 41fa2f64d332a9e3d1b61fd366fa899383493618 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 4 Dec 2014 21:24:09 -0500 Subject: [PATCH 25/33] Fixed GL picking bug --- pyqtgraph/opengl/GLGraphicsItem.py | 5 +++++ pyqtgraph/opengl/GLViewWidget.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py index cdfaa68343..12c5b70711 100644 --- a/pyqtgraph/opengl/GLGraphicsItem.py +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -28,8 +28,13 @@ class GLGraphicsItem(QtCore.QObject): + _nextId = 0 + def __init__(self, parentItem=None): QtCore.QObject.__init__(self) + self._id = GLGraphicsItem._nextId + GLGraphicsItem._nextId += 1 + self.__parent = None self.__view = None self.__children = set() diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 788ab72530..992aa73e38 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -159,7 +159,6 @@ def itemsAt(self, region=None): items = [(h.near, h.names[0]) for h in hits] items.sort(key=lambda i: i[0]) - return [self._itemNames[i[1]] for i in items] def paintGL(self, region=None, viewport=None, useItemNames=False): @@ -193,8 +192,8 @@ def drawItemTree(self, item=None, useItemNames=False): try: glPushAttrib(GL_ALL_ATTRIB_BITS) if useItemNames: - glLoadName(id(i)) - self._itemNames[id(i)] = i + glLoadName(i._id) + self._itemNames[i._id] = i i.paint() except: from .. import debug From f7a54ffd42f55cfcab866967babdd95b1d3f4a73 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Dec 2014 20:01:00 -0500 Subject: [PATCH 26/33] Release 0.9.9 --- doc/source/conf.py | 4 ++-- pyqtgraph/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index bf35651d6b..604ea54966 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -50,9 +50,9 @@ # built documents. # # The short X.Y version. -version = '0.9.8' +version = '0.9.9' # The full version, including alpha/beta/rc tags. -release = '0.9.8' +release = '0.9.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index d539e06be4..0f5333f030 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ www.pyqtgraph.org """ -__version__ = '0.9.8' +__version__ = '0.9.9' ### import all the goodies and add some helper functions for easy CLI use From 9a951318be9a78061a76332349f6d3968813b751 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Dec 2014 18:29:09 -0500 Subject: [PATCH 27/33] Add example subpackages to setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b5cab926e..f1f46f718e 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ import setupHelpers as helpers ## generate list of all sub-packages -allPackages = helpers.listAllPackages(pkgroot='pyqtgraph') + ['pyqtgraph.examples'] +allPackages = (helpers.listAllPackages(pkgroot='pyqtgraph') + + ['pyqtgraph.'+x for x in helpers.listAllPackages(pkgroot='examples')]) ## Decide what version string to use in the build version, forcedVersion, gitVersion, initVersion = helpers.getVersionStrings(pkg='pyqtgraph') From 77906fc7a20917bed2a8fe025e160d2fe2c703db Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 23 Dec 2014 15:55:52 -0500 Subject: [PATCH 28/33] corrections to manifest Add pure-python integrator to verlet chain example --- MANIFEST.in | 2 +- examples/verlet_chain/chain.py | 13 ++++-- examples/verlet_chain/maths.so | Bin 8017 -> 0 bytes examples/verlet_chain/relax.py | 79 ++++++++++++++++++++++++++------- examples/verlet_chain_demo.py | 33 ++++++++++---- 5 files changed, 97 insertions(+), 30 deletions(-) delete mode 100755 examples/verlet_chain/maths.so diff --git a/MANIFEST.in b/MANIFEST.in index c6667d048d..86ae0f60c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include pyqtgraph *.py *.ui *.m README *.txt recursive-include tests *.py *.ui -recursive-include examples *.py *.ui +recursive-include examples *.py *.ui *.gz *.cfg recursive-include doc *.rst *.py *.svg *.png *.jpg recursive-include doc/build/html * recursive-include tools * diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 896505ac75..6eb3501ab2 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -1,7 +1,7 @@ import pyqtgraph as pg import numpy as np import time -from .relax import relax +from . import relax class ChainSim(pg.QtCore.QObject): @@ -52,7 +52,7 @@ def init(self): self.mrel1[self.fixed[l2]] = 0 self.mrel2 = 1.0 - self.mrel1 - for i in range(100): + for i in range(10): self.relax(n=10) self.initialized = True @@ -75,6 +75,10 @@ def update(self): else: dt = now - self.lasttime self.lasttime = now + + # limit amount of work to be done between frames + if not relax.COMPILED: + dt = self.maxTimeStep if self.lastpos is None: self.lastpos = self.pos @@ -103,8 +107,9 @@ def update(self): def relax(self, n=50): - # speed up with C magic - relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) + # speed up with C magic if possible + relax.relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) self.relaxed.emit() + diff --git a/examples/verlet_chain/maths.so b/examples/verlet_chain/maths.so deleted file mode 100755 index 62aff3214ec02e8d87bef57c290d33d7efc7f472..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8017 zcmeHMeN0=|6~D$1AP{VlCS##fyeL&kH6D!RgC!${z=M~_BpJn8GBtZOwgFGZCiZg* z-4_{Y6vw5aRW;?GN+{B%sGX)ri?+0lHj`{^R;?24)Co=fgLSF~QKVA#A-a!v=iGab zdGFb7y8W@ga>4K1^E)5+-23k5yWdm2-6akOqvT`<7;-aZ0%?~5tyX4$w6j)L4$pd4 z$91LZnu00!-g?0hWz53?EMpz!YB&qjBQlaUmk731QnEu9?dqgmozy3qkyRmDA>6Q1 zp!mBb<#xJ5>JddtUx4axKP)lHFIqj@M7h??ouiK3QI|c45>WlFI7v zx;+4eIN{fG#K(Gny7Sb8x2o#EhtK`+)nDv=INY`HCdPnrEQ{LzLT0;zm9|$RhE=SF z-$C`=JFore`ESpkI{x4*Qyg*TW7#_@f5Ja@(bbqBKWre zFBCv)5&b^Ex5Lk#E&$+Wn_08lV-dZ`@hz;?hCe59yFSLUu|R#daJ-s%Wq$#drzAW# zvMM$sJH^ZRA~5Ot&`2z*Ck%hw&~>JVqhW*TgFu*msJ~YahT@^2aKZ@1`+GYhv1q8@ zKM)BCSz(DD81th8e}no_el?(5q}~PO0ak+;vZv)Q*nbu!UF*%5mWXsJrwjC zeu!rvkr3ek6b-T-@1cX8dW+Jc>=qH{o+Z$W*8T*H`~+m_T_v}MD;ad!OJpU-EA{tL z*&Y=(yjkK6@_mp#@$)VZ_lRmVBoJ6I;nc*4FPd-~qlhn?a9Je6Y}JI9b3{DqWITwO zySm4OoAtHHI~9w2L0OypRmDxlvb&#O?_t@8UVx`-TRY^CA4ca(3t31HT|gd($I=|I zXs@Nqb_1wAoiR$XbKKFOYuj10VcJyLD9WbV27vgqovT{v18s7(=E;(iH^K0)mBMi4 zWOf0|1N|=x{T7q{?5a~s-Oy%lKdL$AwAAa`+jo=Pe)Dg+{W}KOzmN74Z65=|k`HT> zZ9m7H56UyDwRGDbfLm;XkQayHaq{)DIRG4gxjBeQ$;CU_cCj4HjBOCy5NKenHu)g_ z?*k0JvU4Ywz6K7K`rt7*jqbHGcc!tbsqb9YQpp)D<-4e*dZ)c9^}ILLJMo5k1*B{# z3h(<3^(xQzK|ZZs)j$!d^?s?AR%ftkY39hJ)N4XCyHKi49fRgI%dV%@YhX6@z^~B} z$Sw;zEv4QPqREV-pm;8=UN2%fFGR&G7gk(ub$-S5xO!{FRjV!{3)ti89J0&EE)KdH zKzO2;3jsQT`0?4r*Z!T&g4WYx&|F$tkd&Ii8U~=g>I8)E`WZW$N$wN1UaQ%85P@$t z=u*_o>H0!qXfUYwnm2dO+kryr;H?6q&4A0^ zyzZP`F7&F>zH3_G9c`-mUGe^W@c!NeH|x_rXc4nuS_n$eJvQ&#Q1%?q8&DK<9_$7A zFHE|hya9dSzbj%nzli+qlJ;G<23a)vi{|LDCy1!gWud6K+fNy_*){KE=z3DU>VQ|i zU#)XJbLn5%?4)`H_$&KWkL#Ip!2jmvG@q-)G{+qE&i%OdYGzNKoj#5Iy`e58Y64Hl*U_+eH)cx_ee_Uu%{+j_% z1F!y(Z~%&ofg^Y*+&e!cDR4$&N+32e5{er3ru(0G952}CsGk=5K0(f9HzJlPko!CI zDYB<=MD#0CllmL=XL5k+Gmas$r*TH~nC!?{F6xjy_5XIj&^OuBxFt&C6jXSkaY4KW z85}cYPve9r%@-6u=@IpTJ&r-*X&e&u$bv9_ESut&FbhJ4>V_DNd!iWxAyN5cPxX(2 z%xq8dNRD!AVUKf{-F^%(jEm+unrDgDNP8k!mN_MWG24&IaZ8luJ+g7j4AJk}>}RAs z(I49iBs=nV)@D!Z0#TYTN#Ev<;ddE~pWa`w?`*~FFWT(K-3#8gU%sQ zyHok}9&drnTt0nYna4^&Y7iv%BzvN7fy8W2>n?o=x|jX$ZT7U@G{^v{916hBzXt-u zsQ&c5uDOUkwFCM4BV;iCW&$K7`$udz>S7{V3wbJ=3*_VLvi#d-5b|V4F!I#*2}4>T zCzy6Q9&sP_1;kMZiRK?1UxmuoKF8HqrOmm$e4$nN>4a!$Ju+)JA!1rzthkfWx?#mj z*+hXLove(}Ja5%s$7uex;^n#d*@{=>_HR}^|GPBGo$$q*=0~eOe5aM|ZN>Ay>k2;` za`yvP{TfE|j}?bOCC9fFcQcwdtavS>`M`>AgnJN=8RB^-+r()6nuSi>^~(LQ72nKg zyjk%rdEd!V?qsxEUem_@RLVZet|21V1*8POd)wNxzlgpLxP#@_HzNIHY<|v2eA$M- zAaVP-<|XcbtzI+^Ug3Ct`!7g8bWQ@6r#tX;;EZo;|0-};s2z&ccN6G4V3C=Z>y>oR zKF{&|c0hR{|BYNf|2^*Gcz!?Wl=?JJQn^Ptp5ISGfO|;EM9%58$N`-sy;2A`_(R!I(Z2i4FK8deDf)6S_Y+#^BU4 z8VMPppt5K0u3g}{fD*rd5~m@!12W>{Oq`#B$&rz9Ffnob!pfq`Z lengths)) + ##dist[mask] = lengths[mask] + #change = (lengths-dist) / dist + #change[mask] = 0 + + #dx *= change[:, np.newaxis] + #print dx + + ##pos[p1] -= mrel2 * dx + ##pos[p2] += mrel1 * dx + #for j in range(links.shape[0]): + #pos[links[j,0]] -= mrel2[j] * dx[j] + #pos[links[j,1]] += mrel1[j] * dx[j] + + + for l in range(links.shape[0]): + p1, p2 = links[l]; + x1 = pos[p1] + x2 = pos[p2] + + dx = x2 - x1 + dist2 = (dx**2).sum() + + if (push[l] and dist2 < lengths2[l]) or (pull[l] and dist2 > lengths2[l]): + dist = dist2 ** 0.5 + change = (lengths[l]-dist) / dist + dx *= change + pos[p1] -= mrel2[l] * dx + pos[p2] += mrel1[l] * dx diff --git a/examples/verlet_chain_demo.py b/examples/verlet_chain_demo.py index 6ed97d48f4..1197344de4 100644 --- a/examples/verlet_chain_demo.py +++ b/examples/verlet_chain_demo.py @@ -1,26 +1,38 @@ """ Mechanical simulation of a chain using verlet integration. +Use the mouse to interact with one of the chains. +By default, this uses a slow, pure-python integrator to solve the chain link +positions. Unix users may compile a small math library to speed this up by +running the `examples/verlet_chain/make` script. """ + import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -from verlet_chain import ChainSim - -sim = ChainSim() +import verlet_chain +sim = verlet_chain.ChainSim() -chlen1 = 80 -chlen2 = 60 +if verlet_chain.relax.COMPILED: + # Use more complex chain if compiled mad library is available. + chlen1 = 80 + chlen2 = 60 + linklen = 1 +else: + chlen1 = 10 + chlen2 = 8 + linklen = 8 + npts = chlen1 + chlen2 sim.mass = np.ones(npts) -sim.mass[chlen1-15] = 100 +sim.mass[int(chlen1 * 0.8)] = 100 sim.mass[chlen1-1] = 500 sim.mass[npts-1] = 200 @@ -31,8 +43,10 @@ sim.pos = np.empty((npts, 2)) sim.pos[:chlen1, 0] = 0 sim.pos[chlen1:, 0] = 10 -sim.pos[:chlen1, 1] = np.arange(chlen1) -sim.pos[chlen1:, 1] = np.arange(chlen2) +sim.pos[:chlen1, 1] = np.arange(chlen1) * linklen +sim.pos[chlen1:, 1] = np.arange(chlen2) * linklen +# to prevent miraculous balancing acts: +sim.pos += np.random.normal(size=sim.pos.shape, scale=1e-3) links1 = [(j, i+j+1) for i in range(chlen1) for j in range(chlen1-i-1)] links2 = [(j, i+j+1) for i in range(chlen2) for j in range(chlen2-i-1)] @@ -55,7 +69,8 @@ sim.pull = np.ones(sim.links.shape[0], dtype=bool) sim.pull[-1] = False -mousepos = sim.pos[0] +# move chain initially just to generate some motion if the mouse is not over the window +mousepos = np.array([30, 20]) def display(): From 930c3a1c40c7430174b8e191086701522599a7dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 23 Dec 2014 16:39:37 -0500 Subject: [PATCH 29/33] Add example data files to setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f1f46f718e..4c1a6acae6 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ def run(self): 'style': helpers.StyleCommand}, packages=allPackages, package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source - #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, + package_data={'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg']}, install_requires = [ 'numpy', ], From 2357cb427f2344e3fbe726882d320913c3143ae6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Dec 2014 11:00:00 -0500 Subject: [PATCH 30/33] correction for setup version string detection --- tools/setupHelpers.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index b308b2261b..ef711b842e 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -360,12 +360,9 @@ def getGitVersion(tagPrefix): # Find last tag matching "tagPrefix.*" tagNames = check_output(['git', 'tag'], universal_newlines=True).strip().split('\n') - while True: - if len(tagNames) == 0: - raise Exception("Could not determine last tagged version.") - lastTagName = tagNames.pop() - if re.match(tagPrefix+r'\d+\.\d+.*', lastTagName): - break + tagNames = [x for x in tagNames if re.match(tagPrefix + r'\d+\.\d+\..*', x)] + tagNames.sort(key=lambda s: map(int, s[len(tagPrefix):].split('.'))) + lastTagName = tagNames[-1] gitVersion = lastTagName.replace(tagPrefix, '') # is this commit an unchanged checkout of the last tagged version? From 305dc7468e142d8d2756686664ca0b388d6448c0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Dec 2014 11:05:05 -0500 Subject: [PATCH 31/33] manifest corrections --- MANIFEST.in | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 86ae0f60c5..9b3331b342 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ -recursive-include pyqtgraph *.py *.ui *.m README *.txt -recursive-include tests *.py *.ui +recursive-include pyqtgraph *.py *.ui *.m README.* *.txt recursive-include examples *.py *.ui *.gz *.cfg -recursive-include doc *.rst *.py *.svg *.png *.jpg +recursive-include doc *.rst *.py *.svg *.png recursive-include doc/build/html * recursive-include tools * include doc/Makefile doc/make.bat README.md LICENSE.txt CHANGELOG From 9a15b557060543e56bebe72b5dc6657e1152996d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Dec 2014 11:19:42 -0500 Subject: [PATCH 32/33] Correct setup to use new setuptools if it is available --- setup.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 4c1a6acae6..d31ec82c49 100644 --- a/setup.py +++ b/setup.py @@ -34,14 +34,18 @@ ) -from distutils.core import setup import distutils.dir_util import os, sys, re try: - # just avoids warning about install_requires import setuptools + from setuptools import setup + from setuptools.command import build + from setuptools.command import install except ImportError: - pass + from distutils.core import setup + from distutils.command import build + from distutils.command import install + path = os.path.split(__file__)[0] sys.path.insert(0, os.path.join(path, 'tools')) @@ -55,9 +59,8 @@ version, forcedVersion, gitVersion, initVersion = helpers.getVersionStrings(pkg='pyqtgraph') -import distutils.command.build -class Build(distutils.command.build.build): +class Build(build.build): """ * Clear build path before building * Set version string in __init__ after building @@ -71,7 +74,7 @@ def run(self): if os.path.isdir(buildPath): distutils.dir_util.remove_tree(buildPath) - ret = distutils.command.build.build.run(self) + ret = build.build.run(self) # If the version in __init__ is different from the automatically-generated # version string, then we will update __init__ in the build directory @@ -94,9 +97,8 @@ def run(self): sys.excepthook(*sys.exc_info()) return ret -import distutils.command.install -class Install(distutils.command.install.install): +class Install(install.install): """ * Check for previously-installed version before installing """ @@ -108,7 +110,8 @@ def run(self): "installed at %s; remove this before installing." % (name, path)) print("Installing to %s" % path) - return distutils.command.install.install.run(self) + return install.install.run(self) + setup( version=version, From e8820667f036450123cab48f4aeee314a0988b62 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Dec 2014 12:06:40 -0500 Subject: [PATCH 33/33] setup correction --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d31ec82c49..7ca1be2657 100644 --- a/setup.py +++ b/setup.py @@ -35,15 +35,14 @@ import distutils.dir_util +from distutils.command import build import os, sys, re try: import setuptools from setuptools import setup - from setuptools.command import build from setuptools.command import install except ImportError: from distutils.core import setup - from distutils.command import build from distutils.command import install