From e5f383fbb5e3953d44b9767d3a73b32d2a32e480 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Nov 2012 16:01:25 -0500 Subject: [PATCH] Bugfixes and updates to functions.py: - generalized makeARGB API: can now process arrays of arbitrary shape. - affineSlice automatically converts vector arguments to array - new function applyLookupTable taken from makeARGB - isosurface function returns array Updated VideoSpeedTest example to follow new makeARGB API LayoutWidget: row argument now accepts 'next' as value ParameterTree bugfix: avoid infinite recursion when accessing non-existent attributes ViewBox: avoid exit error caused when cleanup callback is invoked while python is shutting down --- examples/VideoSpeedTest.py | 26 ++- examples/VideoTemplate.ui | 10 +- examples/VideoTemplate_pyqt.py | 14 +- examples/VideoTemplate_pyside.py | 14 +- functions.py | 367 ++++++++++++++++++------------- graphicsItems/GraphicsItem.py | 8 +- graphicsItems/ViewBox/ViewBox.py | 4 +- parametertree/Parameter.py | 7 +- widgets/LayoutWidget.py | 7 +- 9 files changed, 273 insertions(+), 184 deletions(-) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index d51798bd..0a20e9cf 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -61,37 +61,41 @@ ui.alphaCheck.toggled.connect(updateLUT) def updateScale(): global ui spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3] - if ui.rgbCheck.isChecked(): + if ui.rgbLevelsCheck.isChecked(): for s in spins[2:]: s.setEnabled(True) else: for s in spins[2:]: s.setEnabled(False) -ui.rgbCheck.toggled.connect(updateScale) +ui.rgbLevelsCheck.toggled.connect(updateScale) cache = {} def mkData(): global data, cache, ui - dtype = ui.dtypeCombo.currentText() + dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked()) if dtype not in cache: - if dtype == 'uint8': + if dtype[0] == 'uint8': dt = np.uint8 loc = 128 scale = 64 mx = 255 - elif dtype == 'uint16': + elif dtype[0] == 'uint16': dt = np.uint16 loc = 4096 scale = 1024 mx = 2**16 - elif dtype == 'float': + elif dtype[0] == 'float': dt = np.float loc = 1.0 scale = 0.1 - data = np.random.normal(size=(20,512,512), loc=loc, scale=scale) - data = ndi.gaussian_filter(data, (0, 3, 3)) - if dtype != 'float': + if ui.rgbCheck.isChecked(): + data = np.random.normal(size=(20,512,512,3), loc=loc, scale=scale) + data = ndi.gaussian_filter(data, (0, 6, 6, 0)) + else: + data = np.random.normal(size=(20,512,512), loc=loc, scale=scale) + data = ndi.gaussian_filter(data, (0, 6, 6)) + if dtype[0] != 'float': data = np.clip(data, 0, mx) data = data.astype(dt) cache[dtype] = data @@ -100,7 +104,7 @@ def mkData(): updateLUT() mkData() ui.dtypeCombo.currentIndexChanged.connect(mkData) - +ui.rgbCheck.toggled.connect(mkData) ptr = 0 lastTime = ptime.time() @@ -113,7 +117,7 @@ def update(): useLut = None if ui.scaleCheck.isChecked(): - if ui.rgbCheck.isChecked(): + if ui.rgbLevelsCheck.isChecked(): useScale = [ [ui.minSpin1.value(), ui.maxSpin1.value()], [ui.minSpin2.value(), ui.maxSpin2.value()], diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui index 078e7ccf..3dddb928 100644 --- a/examples/VideoTemplate.ui +++ b/examples/VideoTemplate.ui @@ -25,7 +25,6 @@ 0 - fpsLabel @@ -84,7 +83,7 @@ - + RGB @@ -218,6 +217,13 @@ + + + + RGB + + + diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py index 21e66635..c3430e2d 100644 --- a/examples/VideoTemplate_pyqt.py +++ b/examples/VideoTemplate_pyqt.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file './examples/VideoTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 +# Created: Sun Nov 4 18:24:20 2012 # by: PyQt4 UI code generator 4.9.1 # # WARNING! All changes made in this file will be lost! @@ -55,9 +55,9 @@ class Ui_MainWindow(object): self.scaleCheck = QtGui.QCheckBox(self.centralwidget) self.scaleCheck.setObjectName(_fromUtf8("scaleCheck")) self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) - self.rgbCheck = QtGui.QCheckBox(self.centralwidget) - self.rgbCheck.setObjectName(_fromUtf8("rgbCheck")) - self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName(_fromUtf8("rgbLevelsCheck")) + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) self.minSpin1 = SpinBox(self.centralwidget) @@ -124,6 +124,9 @@ class Ui_MainWindow(object): self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) self.fpsLabel.setObjectName(_fromUtf8("fpsLabel")) self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName(_fromUtf8("rgbCheck")) + self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) @@ -138,12 +141,13 @@ class Ui_MainWindow(object): self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) - self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py index 5cbce05c..d19e0f23 100644 --- a/examples/VideoTemplate_pyside.py +++ b/examples/VideoTemplate_pyside.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file './examples/VideoTemplate.ui' # -# Created: Sun Sep 9 14:41:31 2012 +# Created: Sun Nov 4 18:24:21 2012 # by: pyside-uic 0.2.13 running on PySide 1.1.0 # # WARNING! All changes made in this file will be lost! @@ -50,9 +50,9 @@ class Ui_MainWindow(object): self.scaleCheck = QtGui.QCheckBox(self.centralwidget) self.scaleCheck.setObjectName("scaleCheck") self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) - self.rgbCheck = QtGui.QCheckBox(self.centralwidget) - self.rgbCheck.setObjectName("rgbCheck") - self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.minSpin1 = SpinBox(self.centralwidget) @@ -119,6 +119,9 @@ class Ui_MainWindow(object): self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) self.fpsLabel.setObjectName("fpsLabel") self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) @@ -133,12 +136,13 @@ class Ui_MainWindow(object): self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) - self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget diff --git a/functions.py b/functions.py index 1342b527..4eafbbf3 100644 --- a/functions.py +++ b/functions.py @@ -434,8 +434,10 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, ## make sure vectors are arrays - vectors = np.array(vectors) - origin = np.array(origin) + if not isinstance(vectors, np.ndarray): + vectors = np.array(vectors) + if not isinstance(origin, np.ndarray): + origin = np.array(origin) origin.shape = (len(axes),) + (1,)*len(shape) ## Build array of sample locations. @@ -580,171 +582,247 @@ def solveBilinearTransform(points1, points2): return matrix +def rescaleData(data, scale, offset, dtype=None): + """Return data rescaled and optionally cast to a new dtype:: + data => (data-offset) * scale + + Uses scipy.weave (if available) to improve performance. + """ + global USE_WEAVE + if dtype is None: + dtype = data.dtype + try: + if not USE_WEAVE: + raise Exception('Weave is disabled; falling back to slower version.') + + newData = np.empty((data.size,), dtype=dtype) + flat = np.ascontiguousarray(data).reshape(data.size) + size = data.size + + code = """ + double sc = (double)scale; + double off = (double)offset; + for( int i=0; i0 and max->*scale*:: - For 3D arrays (x, y, rgba): - * The third axis must have length 3 or 4 and will be interpreted as RGBA. - * The 'lut' argument is not allowed. + rescaled = (clip(data, min, max) - min) * (*scale* / (max - min)) - lut - Lookup table for 2D data. May be 1D or 2D (N,rgba) and must have dtype=ubyte. - Values in data will be converted to color by indexing directly from lut. - Lookup tables can be built using GradientWidget. - levels - List [min, max]; optionally rescale data before converting through the - lookup table. rescaled = (data-min) * len(lut) / (max-min) - useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is - False, which returns in BGRA order for use with QImage. - + It is also possible to use a 2D (N,2) array of values for levels. In this case, + it is assumed that each pair of min,max values in the levels array should be + applied to a different subset of the input data (for example, the input data may + already have RGB values and the levels are used to independently scale each + channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. + scale The maximum value to which data will be rescaled before being passed through the + lookup table (or returned if there is no lookup table). By default this will + be set to the length of the lookup table, or 256 is no lookup table is provided. + For OpenGL color specifications (as in GLColor4f) use scale=1.0 + lut Optional lookup table (array with dtype=ubyte). + Values in data will be converted to color by indexing directly from lut. + The output data shape will be input.shape + lut.shape[1:]. + + Note: the output of makeARGB will have the same dtype as the lookup table, so + for conversion to QImage, the dtype must be ubyte. + + Lookup tables can be built using GradientWidget. + useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). + The default is False, which returns in ARGB order for use with QImage + (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + is BGRA). + ============ ================================================================================== """ prof = debug.Profiler('functions.makeARGB', disabled=True) + if lut is not None and not isinstance(lut, np.ndarray): + lut = np.array(lut) + if levels is not None and not isinstance(levels, np.ndarray): + levels = np.array(levels) + ## sanity checks - if data.ndim == 3: - if data.shape[2] not in (3,4): - raise Exception("data.shape[2] must be 3 or 4") - #if lut is not None: - #raise Exception("can not use lookup table with 3D data") - elif data.ndim != 2: - raise Exception("data must be 2D or 3D") + #if data.ndim == 3: + #if data.shape[2] not in (3,4): + #raise Exception("data.shape[2] must be 3 or 4") + ##if lut is not None: + ##raise Exception("can not use lookup table with 3D data") + #elif data.ndim != 2: + #raise Exception("data must be 2D or 3D") - if lut is not None: - if lut.ndim == 2: - if lut.shape[1] not in (3,4): - raise Exception("lut.shape[1] must be 3 or 4") - elif lut.ndim != 1: - raise Exception("lut must be 1D or 2D") - if lut.dtype != np.ubyte: - raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) + #if lut is not None: + ##if lut.ndim == 2: + ##if lut.shape[1] : + ##raise Exception("lut.shape[1] must be 3 or 4") + ##elif lut.ndim != 1: + ##raise Exception("lut must be 1D or 2D") + #if lut.dtype != np.ubyte: + #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) + if levels is not None: - levels = np.array(levels) - if levels.shape == (2,): - pass - elif levels.shape in [(3,2), (4,2)]: - if data.ndim == 3: - raise Exception("Can not use 2D levels with 3D data.") - if lut is not None: - raise Exception('Can not use 2D levels and lookup table together.') + if levels.ndim == 1: + if len(levels) != 2: + raise Exception('levels argument must have length 2') + elif levels.ndim == 2: + if lut is not None and lut.ndim > 1: + raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2') + if levels.shape != (data.shape[-1], 2): + raise Exception('levels must have shape (data.shape[-1], 2)') else: - raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") + print levels + raise Exception("levels argument must be 1D or 2D.") + #levels = np.array(levels) + #if levels.shape == (2,): + #pass + #elif levels.shape in [(3,2), (4,2)]: + #if data.ndim == 3: + #raise Exception("Can not use 2D levels with 3D data.") + #if lut is not None: + #raise Exception('Can not use 2D levels and lookup table together.') + #else: + #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") prof.mark('1') - if lut is not None: - lutLength = lut.shape[0] - else: - lutLength = 256 - - ## weave requires contiguous arrays - global USE_WEAVE - if (levels is not None or lut is not None) and USE_WEAVE: - data = np.ascontiguousarray(data) + if scale is None: + if lut is not None: + scale = lut.shape[0] + else: + scale = 255. ## Apply levels if given if levels is not None: - try: ## use weave to speed up scaling - if not USE_WEAVE: - raise Exception('Weave is disabled; falling back to slower version.') - if levels.ndim == 1: - scale = float(lutLength) / (levels[1]-levels[0]) - offset = float(levels[0]) - data = rescaleData(data, scale, offset) - else: - if data.ndim == 2: - newData = np.empty(data.shape+(levels.shape[0],), dtype=np.uint32) - for i in range(levels.shape[0]): - scale = float(lutLength / (levels[i,1]-levels[i,0])) - offset = float(levels[i,0]) - newData[...,i] = rescaleData(data, scale, offset) - elif data.ndim == 3: - newData = np.empty(data.shape, dtype=np.uint32) - for i in range(data.shape[2]): - scale = float(lutLength / (levels[i,1]-levels[i,0])) - offset = float(levels[i,0]) - #print scale, offset, data.shape, newData.shape, levels.shape - newData[...,i] = rescaleData(data[...,i], scale, offset) - data = newData - except: - if USE_WEAVE: - debug.printExc("Error; disabling weave.") - USE_WEAVE = False - - if levels.ndim == 1: - if data.ndim == 2: - levels = levels[np.newaxis, np.newaxis, :] - else: - levels = levels[np.newaxis, np.newaxis, np.newaxis, :] - else: - levels = levels[np.newaxis, np.newaxis, ...] - if data.ndim == 2: - data = data[..., np.newaxis] - data = ((data-levels[...,0]) * lutLength) / (levels[...,1]-levels[...,0]) + if isinstance(levels, np.ndarray) and levels.ndim == 2: + ## we are going to rescale each channel independently + if levels.shape[0] != data.shape[-1]: + raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])") + newData = np.empty(data.shape, dtype=int) + for i in range(data.shape[-1]): + minVal, maxVal = levels[i] + if minVal == maxVal: + maxVal += 1e-16 + newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int) + data = newData + else: + minVal, maxVal = levels + if minVal == maxVal: + maxVal += 1e-16 + data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) prof.mark('2') ## apply LUT if given - if lut is not None and data.ndim == 2: - - if data.dtype.kind not in ('i', 'u'): - data = data.astype(int) - - data = np.clip(data, 0, lutLength-1) - try: - if not USE_WEAVE: - raise Exception('Weave is disabled; falling back to slower version.') - - newData = np.empty((data.size,) + lut.shape[1:], dtype=np.uint8) - flat = data.reshape(data.size) - size = data.size - ncol = lut.shape[1] - newStride = newData.strides[0] - newColStride = newData.strides[1] - lutStride = lut.strides[0] - lutColStride = lut.strides[1] - flatStride = flat.strides[0] / flat.dtype.itemsize - - #print "newData:", newData.shape, newData.dtype - #print "flat:", flat.shape, flat.dtype, flat.min(), flat.max() - #print "lut:", lut.shape, lut.dtype - #print "size:", size, "ncols:", ncol - #print "strides:", newStride, newColStride, lutStride, lutColStride, flatStride - - code = """ - - for( int i=0; i