diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 9742568a..c4dc64aa 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -6,6 +6,7 @@ Contents: .. toctree:: :maxdepth: 2 + config_options functions graphicsItems/index widgets/index diff --git a/doc/source/config_options.rst b/doc/source/config_options.rst new file mode 100644 index 00000000..61b64499 --- /dev/null +++ b/doc/source/config_options.rst @@ -0,0 +1,41 @@ +.. currentmodule:: pyqtgraph + +.. _apiref_config: + +Global Configuration Options +============================ + +PyQtGraph has several global configuration options that allow you to change its +default behavior. These can be accessed using the :func:`setConfigOptions` and +:func:`getConfigOption` functions: + +================== =================== ================== ================================================================================ +**Option** **Type** **Default** +leftButtonPan bool True If True, dragging the left mouse button over a ViewBox + causes the view to be panned. If False, then dragging + the left mouse button draws a rectangle that the + ViewBox will zoom to. +foreground See :func:`mkColor` 'd' Default foreground color for text, lines, axes, etc. +background See :func:`mkColor` 'k' Default background for :class:`GraphicsView`. +antialias bool False Enabling antialiasing causes lines to be drawn with + smooth edges at the cost of reduced performance. +imageAxisOrder str 'col-major' For 'row-major', image data is expected in the standard row-major + (row, col) order. For 'col-major', image data is expected in + reversed column-major (col, row) order. + The default is 'col-major' for backward compatibility, but this may + change in the future. +editorCommand str or None None Command used to invoke code editor from ConsoleWidget. +exitCleanup bool True Attempt to work around some exit crash bugs in PyQt and PySide. +useWeave bool False Use weave to speed up some operations, if it is available. +weaveDebug bool False Print full error message if weave compile fails. +useOpenGL bool False Enable OpenGL in GraphicsView. This can have unpredictable effects on stability + and performance. +enableExperimental bool False Enable experimental features (the curious can search for this key in the code). +crashWarning bool False If True, print warnings about situations that may result in a crash. +================== =================== ================== ================================================================================ + + +.. autofunction:: pyqtgraph.setConfigOptions + +.. autofunction:: pyqtgraph.getConfigOption + diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 86c2564b..b911cec8 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -2,7 +2,7 @@ """ This example demonstrates a very basic use of flowcharts: filter data, displaying both the input and output of the filter. The behavior of -he filter can be reprogrammed by the user. +the filter can be reprogrammed by the user. Basic steps are: - create a flowchart and two plots diff --git a/examples/ImageView.py b/examples/ImageView.py index 881d8cdd..3412f348 100644 --- a/examples/ImageView.py +++ b/examples/ImageView.py @@ -17,6 +17,9 @@ import numpy as np from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg +# Interpret image data as row-major instead of col-major +pg.setConfigOptions(imageAxisOrder='row-major') + app = QtGui.QApplication([]) ## Create window with ImageView widget @@ -42,7 +45,7 @@ sig[40:] += np.exp(-np.linspace(1,10, 60)) sig[70:] += np.exp(-np.linspace(1,10, 30)) sig = sig[:,np.newaxis,np.newaxis] * 3 -data[:,50:60,50:60] += sig +data[:,50:60,30:40] += sig ## Display the data and assign each frame a time value from 1.0 to 3.0 diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 55c671ad..a48fa7b5 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -11,6 +11,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np +pg.setConfigOptions(imageAxisOrder='row-major') ## Create image to display arr = np.ones((100, 100), dtype=float) @@ -24,6 +25,11 @@ arr[:, 50] = 10 arr += np.sin(np.linspace(0, 20, 100)).reshape(1, 100) arr += np.random.normal(size=(100,100)) +# add an arrow for asymmetry +arr[10, :50] = 10 +arr[9:12, 44:48] = 10 +arr[8:13, 44:46] = 10 + ## create GUI app = QtGui.QApplication([]) diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 95b938cd..9e67ebe1 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -8,23 +8,15 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg +pg.setConfigOptions(imageAxisOrder='row-major') + ## create GUI app = QtGui.QApplication([]) w = pg.GraphicsWindow(size=(800,800), border=True) - v = w.addViewBox(colspan=2) - -#w = QtGui.QMainWindow() -#w.resize(800,800) -#v = pg.GraphicsView() v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) -#v.enableMouse(True) -#v.autoPixelScale = False -#w.setCentralWidget(v) -#s = v.scene() -#v.setRange(QtCore.QRectF(-2, -2, 220, 220)) ## Create image to display @@ -37,6 +29,11 @@ arr[:, 75] = 5 arr[50, :] = 10 arr[:, 50] = 10 +# add an arrow for asymmetry +arr[10, :50] = 10 +arr[9:12, 44:48] = 10 +arr[8:13, 44:46] = 10 + ## Create image items, add to scene and set position im1 = pg.ImageItem(arr) im2 = pg.ImageItem(arr) @@ -44,6 +41,7 @@ v.addItem(im1) v.addItem(im2) im2.moveBy(110, 20) v.setRange(QtCore.QRectF(0, 0, 200, 120)) +im1.scale(0.8, 0.5) im3 = pg.ImageItem() v2 = w.addViewBox(1,0) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index d26f507e..3516472f 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -103,6 +103,9 @@ def mkData(): if dtype[0] != 'float': data = np.clip(data, 0, mx) data = data.astype(dt) + data[:, 10, 10:50] = mx + data[:, 9:12, 48] = mx + data[:, 8:13, 47] = mx cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration) data = cache[dtype] diff --git a/examples/imageAnalysis.py b/examples/imageAnalysis.py index 8283144e..13adf5ac 100644 --- a/examples/imageAnalysis.py +++ b/examples/imageAnalysis.py @@ -12,8 +12,11 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -pg.mkQApp() +# Interpret image data as row-major instead of col-major +pg.setConfigOptions(imageAxisOrder='row-major') + +pg.mkQApp() win = pg.GraphicsLayoutWidget() win.setWindowTitle('pyqtgraph example: Image Analysis') @@ -57,10 +60,10 @@ win.show() # Generate image data -data = np.random.normal(size=(100, 200)) +data = np.random.normal(size=(200, 100)) data[20:80, 20:80] += 2. data = pg.gaussianFilter(data, (3, 3)) -data += np.random.normal(size=(100, 200)) * 0.1 +data += np.random.normal(size=(200, 100)) * 0.1 img.setImage(data) hist.setLevels(data.min(), data.max()) @@ -79,7 +82,7 @@ p1.autoRange() def updatePlot(): global img, roi, data, p2 selected = roi.getArrayRegion(data, img) - p2.plot(selected.mean(axis=1), clear=True) + p2.plot(selected.mean(axis=0), clear=True) roi.sigRegionChanged.connect(updatePlot) updatePlot() diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index 23281343..b1aea297 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np + class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. @@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform): def matrix(self): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) + if __name__ == '__main__': from . import widgets diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 9aafa5b5..5b17297f 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -59,16 +59,32 @@ CONFIG_OPTIONS = { 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'crashWarning': False, # If True, print warnings about situations that may result in a crash + 'imageAxisOrder': 'col-major', # For 'row-major', image data is expected in the standard (row, col) order. + # For 'col-major', image data is expected in reversed (col, row) order. + # The default is 'col-major' for backward compatibility, but this may + # change in the future. } def setConfigOption(opt, value): + global CONFIG_OPTIONS + if opt not in CONFIG_OPTIONS: + raise KeyError('Unknown configuration option "%s"' % opt) + if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'): + raise ValueError('imageAxisOrder must be either "row-major" or "col-major"') CONFIG_OPTIONS[opt] = value def setConfigOptions(**opts): - CONFIG_OPTIONS.update(opts) + """Set global configuration options. + + Each keyword argument sets one global option. + """ + for k,v in opts.items(): + setConfigOption(k, v) def getConfigOption(opt): + """Return the value of a single global configuration option. + """ return CONFIG_OPTIONS[opt] diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ad398079..d79c350f 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -959,6 +959,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): elif data.dtype.kind == 'i': s = 2**(data.itemsize*8 - 1) levels = np.array([-s, s-1]) + elif data.dtype.kind == 'b': + levels = np.array([0,1]) else: raise Exception('levels argument is required for float input types') if not isinstance(levels, np.ndarray): @@ -1727,7 +1729,7 @@ def isosurface(data, level): See Paul Bourke, "Polygonising a Scalar Field" (http://paulbourke.net/geometry/polygonise/) - *data* 3D numpy array of scalar values + *data* 3D numpy array of scalar values. Must be contiguous. *level* The level at which to generate an isosurface Returns an array of vertex coordinates (Nv, 3) and an array of @@ -2079,7 +2081,10 @@ def isosurface(data, level): else: faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - + # We use strides below, which means we need contiguous array input. + # Ideally we can fix this just by removing the dependency on strides. + if not data.flags['C_CONTIGUOUS']: + raise TypeError("isosurface input data must be c-contiguous.") ## mark everything below the isosurface level mask = data < level diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2ca35193..d45818dc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -37,9 +37,6 @@ class GraphicsItem(object): if register: GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - - - def getViewWidget(self): """ Return the view widget for this item. @@ -95,7 +92,6 @@ class GraphicsItem(object): def forgetViewBox(self): self._viewBox = None - def deviceTransform(self, viewportTransform=None): """ Return the transform that converts local item coordinates to device coordinates (usually pixels). diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index c46dbbbe..31764250 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -179,8 +179,8 @@ class HistogramLUTItem(GraphicsWidget): return self.lut def regionChanged(self): - #if self.imageItem is not None: - #self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelChangeFinished.emit(self) #self.update() diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 13cc9dce..3d45ad77 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -7,6 +7,8 @@ from .. import functions as fn from .. import debug as debug from .GraphicsObject import GraphicsObject from ..Point import Point +from .. import getConfigOption + __all__ = ['ImageItem'] @@ -28,7 +30,6 @@ class ImageItem(GraphicsObject): for controlling the levels and lookup table used to display the image. """ - sigImageChanged = QtCore.Signal() sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu @@ -47,6 +48,8 @@ class ImageItem(GraphicsObject): self.lut = None self.autoDownsample = False + self.axisOrder = getConfigOption('imageAxisOrder') + # In some cases, we use a modified lookup table to handle both rescaling # and LUT more efficiently self._effectiveLut = None @@ -86,12 +89,14 @@ class ImageItem(GraphicsObject): def width(self): if self.image is None: return None - return self.image.shape[0] + axis = 0 if self.axisOrder == 'col-major' else 1 + return self.image.shape[axis] def height(self): if self.image is None: return None - return self.image.shape[1] + axis = 1 if self.axisOrder == 'col-major' else 0 + return self.image.shape[axis] def boundingRect(self): if self.image is None: @@ -147,7 +152,11 @@ class ImageItem(GraphicsObject): self.update() def setOpts(self, update=True, **kargs): - + if 'axisOrder' in kargs: + val = kargs['axisOrder'] + if val not in ('row-major', 'col-major'): + raise ValueError('axisOrder must be either "row-major" or "col-major"') + self.axisOrder = val if 'lut' in kargs: self.setLookupTable(kargs['lut'], update=update) if 'levels' in kargs: @@ -190,7 +199,7 @@ class ImageItem(GraphicsObject): image (numpy array) Specifies the image data. May be 2D (width, height) or 3D (width, height, RGBa). The array dtype must be integer or floating point of any bit depth. For 3D arrays, the third dimension must - be of length 3 (RGB) or 4 (RGBA). + be of length 3 (RGB) or 4 (RGBA). See *notes* below. autoLevels (bool) If True, this forces the image to automatically select levels based on the maximum and minimum values in the data. By default, this argument is true unless the levels argument is @@ -201,12 +210,26 @@ class ImageItem(GraphicsObject): data. By default, this will be set to the minimum and maximum values in the image. If the image array has dtype uint8, no rescaling is necessary. opacity (float 0.0-1.0) - compositionMode see :func:`setCompositionMode ` + compositionMode See :func:`setCompositionMode ` border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the screen resolution. This improves performance for large images and reduces aliasing. ================= ========================================================================= + + + **Notes:** + + For backward compatibility, image data is assumed to be in column-major order (column, row). + However, most image data is stored in row-major order (row, column) and will need to be + transposed before calling setImage():: + + imageitem.setImage(imagedata.T) + + This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or + by changing the ``imageAxisOrder`` :ref:`global configuration option `. + + """ profile = debug.Profiler() @@ -259,6 +282,42 @@ class ImageItem(GraphicsObject): if gotNewData: self.sigImageChanged.emit() + def dataTransform(self): + """Return the transform that maps from this image's input array to its + local coordinate system. + + This transform corrects for the transposition that occurs when image data + is interpreted in row-major order. + """ + # Might eventually need to account for downsampling / clipping here + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def inverseDataTransform(self): + """Return the transform that maps from this image's local coordinate + system to its input array. + + See dataTransform() for more information. + """ + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def mapToData(self, obj): + tr = self.inverseDataTransform() + return tr.map(obj) + + def mapFromData(self, obj): + tr = self.dataTransform() + return tr.map(obj) + def quickMinMax(self, targetSize=1e6): """ Estimate the min/max values of the image data by subsampling. @@ -303,10 +362,12 @@ class ImageItem(GraphicsObject): if w == 0 or h == 0: self.qimage = None return - xds = int(1.0/w) - yds = int(1.0/h) - image = fn.downsample(self.image, xds, axis=0) - image = fn.downsample(image, yds, axis=1) + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] + image = fn.downsample(self.image, xds, axis=axes[0]) + image = fn.downsample(image, yds, axis=axes[1]) + self._lastDownsample = (xds, yds) else: image = self.image @@ -318,20 +379,28 @@ class ImageItem(GraphicsObject): eflsize = 2**(image.itemsize*8) ind = np.arange(eflsize) minlev, maxlev = levels + levdiff = maxlev - minlev + levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 if lut is None: - efflut = fn.rescaleData(ind, scale=255./(maxlev-minlev), + efflut = fn.rescaleData(ind, scale=255./levdiff, offset=minlev, dtype=np.ubyte) else: lutdtype = np.min_scalar_type(lut.shape[0]-1) - efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/(maxlev-minlev), + efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff, offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1)) efflut = lut[efflut] self._effectiveLut = efflut lut = self._effectiveLut levels = None - - argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=levels) + + # Assume images are in column-major order for backward compatibility + # (most images are in row-major order) + + if self.axisOrder == 'col-major': + image = image.transpose((1, 0, 2)[:image.ndim]) + + argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): @@ -347,7 +416,8 @@ class ImageItem(GraphicsObject): p.setCompositionMode(self.paintMode) profile('set comp mode') - p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage) + shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1] + p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage) profile('p.drawImage') if self.border is not None: p.setPen(self.border) @@ -398,6 +468,7 @@ class ImageItem(GraphicsObject): bins = 500 kwds['bins'] = bins + stepData = stepData[np.isfinite(stepData)] hist = np.histogram(stepData, **kwds) return hist[1][:-1], hist[0] @@ -433,21 +504,6 @@ class ImageItem(GraphicsObject): self.qimage = None self.update() - #def mousePressEvent(self, ev): - #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - #self.drawAt(ev.pos(), ev) - #ev.accept() - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - ##print "mouse move", ev.pos() - #if self.drawKernel is not None: - #self.drawAt(ev.pos(), ev) - - #def mouseReleaseEvent(self, ev): - #pass - def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: ev.ignore() @@ -484,24 +540,18 @@ class ImageItem(GraphicsObject): self.menu.remAct = remAct return self.menu - def hoverEvent(self, ev): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. ev.acceptClicks(QtCore.Qt.RightButton) - #self.box.setBrush(fn.mkBrush('w')) elif not ev.isExit() and self.removable: ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks - #else: - #self.box.setBrush(self.brush) - #self.update() - - def tabletEvent(self, ev): - print(ev.device()) - print(ev.pointerType()) - print(ev.pressure()) + pass + #print(ev.device()) + #print(ev.pointerType()) + #print(ev.pressure()) def drawAt(self, pos, ev=None): pos = [int(pos.x()), int(pos.y())] diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 4474e29a..03ebc69f 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -1,5 +1,4 @@ - - +from .. import getConfigOption from .GraphicsObject import * from .. import functions as fn from ..Qt import QtGui, QtCore @@ -9,12 +8,10 @@ class IsocurveItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` - Item displaying an isocurve of a 2D array.To align this item correctly with an - ImageItem,call isocurve.setParentItem(image) + Item displaying an isocurve of a 2D array. To align this item correctly with an + ImageItem, call ``isocurve.setParentItem(image)``. """ - - - def __init__(self, data=None, level=0, pen='w'): + def __init__(self, data=None, level=0, pen='w', axisOrder=None): """ Create a new isocurve item. @@ -25,6 +22,9 @@ class IsocurveItem(GraphicsObject): level The cutoff value at which to draw the isocurve. pen The color of the curve item. Can be anything valid for :func:`mkPen ` + axisOrder May be either 'row-major' or 'col-major'. By default this uses + the ``imageAxisOrder`` + :ref:`global configuration option `. ============== =============================================================== """ GraphicsObject.__init__(self) @@ -32,9 +32,9 @@ class IsocurveItem(GraphicsObject): self.level = level self.data = None self.path = None + self.axisOrder = getConfigOption('imageAxisOrder') if axisOrder is None else axisOrder self.setPen(pen) self.setData(data, level) - def setData(self, data, level=None): """ @@ -54,7 +54,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setLevel(self, level): """Set the level at which the isocurve is drawn.""" @@ -62,7 +61,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setPen(self, *args, **kwargs): """Set the pen used to draw the isocurve. Arguments can be any that are valid @@ -75,18 +73,8 @@ class IsocurveItem(GraphicsObject): for :func:`mkBrush `""" self.brush = fn.mkBrush(*args, **kwargs) self.update() - def updateLines(self, data, level): - ##print "data:", data - ##print "level", level - #lines = fn.isocurve(data, level) - ##print len(lines) - #self.path = QtGui.QPainterPath() - #for line in lines: - #self.path.moveTo(*line[0]) - #self.path.lineTo(*line[1]) - #self.update() self.setData(data, level) def boundingRect(self): @@ -100,7 +88,13 @@ class IsocurveItem(GraphicsObject): if self.data is None: self.path = None return - lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True) + + if self.axisOrder == 'row-major': + data = self.data.T + else: + data = self.data + + lines = fn.isocurve(data, self.level, connected=True, extendToEdge=True) self.path = QtGui.QPainterPath() for line in lines: self.path.moveTo(*line[0]) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 51853c61..81a4e651 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -21,6 +21,7 @@ from math import cos, sin from .. import functions as fn from .GraphicsObject import GraphicsObject from .UIGraphicsItem import UIGraphicsItem +from .. import getConfigOption __all__ = [ 'ROI', @@ -1016,14 +1017,11 @@ class ROI(GraphicsObject): If returnSlice is set to False, the function returns a pair of tuples with the values that would have been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) - If the slice can not be computed (usually because the scene/transforms are not properly + If the slice cannot be computed (usually because the scene/transforms are not properly constructed yet), then the method returns None. """ - #print "getArraySlice" - ## Determine shape of array along ROI axes dShape = (data.shape[axes[0]], data.shape[axes[1]]) - #print " dshape", dShape ## Determine transform that maps ROI bounding box to image coordinates try: @@ -1032,25 +1030,28 @@ class ROI(GraphicsObject): return None ## Modify transform to scale from image coords to data coords - #m = QtGui.QTransform() - tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) - #tr = tr * m + axisOrder = img.axisOrder + if axisOrder == 'row-major': + tr.scale(float(dShape[1]) / img.width(), float(dShape[0]) / img.height()) + else: + tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) ## Transform ROI bounds into data bounds dataBounds = tr.mapRect(self.boundingRect()) - #print " boundingRect:", self.boundingRect() - #print " dataBounds:", dataBounds ## Intersect transformed ROI bounds with data bounds - intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1])) - #print " intBounds:", intBounds + if axisOrder == 'row-major': + intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[1], dShape[0])) + else: + intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1])) ## Determine index values to use when referencing the array. bounds = ( (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) ) - #print " bounds:", bounds + if axisOrder == 'row-major': + bounds = bounds[::-1] if returnSlice: ## Create slice objects @@ -1074,7 +1075,10 @@ class ROI(GraphicsObject): Used to determine the relationship between the ROI and the boundaries of *data*. axes (length-2 tuple) Specifies the axes in *data* that - correspond to the x and y axes of *img*. + correspond to the (x, y) axes of *img*. If the + image's axis order is set to + 'row-major', then the axes are instead specified in + (y, x) order. returnMappedCoords (bool) If True, the array slice is returned along with a corresponding array of coordinates that were used to extract data from the original array. @@ -1099,7 +1103,8 @@ class ROI(GraphicsObject): shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR) if not returnMappedCoords: - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + rgn = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + return rgn else: kwds['returnCoords'] = True result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) @@ -1114,29 +1119,34 @@ class ROI(GraphicsObject): (shape, vectors, origin) to extract a subset of *data* using this ROI and *img* to specify the subset. + If *fromBoundingRect* is True, then the ROI's bounding rectangle is used + rather than the shape of the ROI. + See :func:`getArrayRegion ` for more information. """ if self.scene() is not img.scene(): raise Exception("ROI and target item must be members of the same scene.") - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + origin = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 0))) ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + vx = img.mapToData(self.mapToItem(img, QtCore.QPointF(1, 0))) - origin + vy = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 1))) - origin lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2) - pxLen = img.width() / float(data.shape[axes[0]]) - #img.width is number of pixels, not width of item. - #need pxWidth and pxHeight instead of pxLen ? - sx = pxLen / lvx - sy = pxLen / lvy + #pxLen = img.width() / float(data.shape[axes[0]]) + ##img.width is number of pixels, not width of item. + ##need pxWidth and pxHeight instead of pxLen ? + #sx = pxLen / lvx + #sy = pxLen / lvy + sx = 1.0 / lvx + sy = 1.0 / lvy vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) if fromBoundingRect is True: shape = self.boundingRect().width(), self.boundingRect().height() - origin = self.mapToItem(img, self.boundingRect().topLeft()) + origin = img.mapToData(self.mapToItem(img, self.boundingRect().topLeft())) origin = (origin.x(), origin.y()) else: shape = self.state['size'] @@ -1144,6 +1154,11 @@ class ROI(GraphicsObject): shape = [abs(shape[0]/sx), abs(shape[1]/sy)] + if img.axisOrder == 'row-major': + # transpose output + vectors = vectors[::-1] + shape = shape[::-1] + return shape, vectors, origin def renderShapeMask(self, width, height): @@ -1166,7 +1181,7 @@ class ROI(GraphicsObject): p.translate(-bounds.topLeft()) p.drawPath(shape) p.end() - mask = fn.imageToArray(im)[:,:,0].astype(float) / 255. + mask = fn.imageToArray(im, transpose=True)[:,:,0].astype(float) / 255. return mask def getGlobalTransform(self, relativeTo=None): @@ -1639,6 +1654,8 @@ class MultiRectROI(QtGui.QGraphicsObject): ## make sure orthogonal axis is the same size ## (sometimes fp errors cause differences) + if img.axisOrder == 'row-major': + axes = axes[::-1] ms = min([r.shape[axes[1]] for r in rgns]) sl = [slice(None)] * rgns[0].ndim sl[axes[1]] = slice(0,ms) @@ -1998,7 +2015,7 @@ class PolyLineROI(ROI): p.lineTo(self.handles[0]['item'].pos()) return p - def getArrayRegion(self, data, img, axes=(0,1)): + def getArrayRegion(self, data, img, axes=(0,1), **kwds): """ Return the result of ROI.getArrayRegion(), masked by the shape of the ROI. Values outside the ROI shape are set to 0. @@ -2006,8 +2023,15 @@ class PolyLineROI(ROI): br = self.boundingRect() if br.width() > 1000: raise Exception() - sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True) - mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]]) + sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True, **kwds) + + if img.axisOrder == 'col-major': + mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]]) + else: + mask = self.renderShapeMask(sliced.shape[axes[1]], sliced.shape[axes[0]]) + mask = mask.T + + # reshape mask to ensure it is applied to the correct data axes shape = [1] * data.ndim shape[axes[0]] = sliced.shape[axes[0]] shape[axes[1]] = sliced.shape[axes[1]] @@ -2085,7 +2109,7 @@ class LineSegmentROI(ROI): return p - def getArrayRegion(self, data, img, axes=(0,1)): + def getArrayRegion(self, data, img, axes=(0,1), order=1, **kwds): """ Use the position of this ROI relative to an imageItem to pull a slice from an array. @@ -2101,7 +2125,7 @@ class LineSegmentROI(ROI): for i in range(len(imgPts)-1): d = Point(imgPts[i+1] - imgPts[i]) o = Point(imgPts[i]) - r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=1) + r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, **kwds) rgns.append(r) return np.concatenate(rgns, axis=axes[0]) diff --git a/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/pyqtgraph/graphicsItems/tests/test_ImageItem.py index d13d703c..4f310bc3 100644 --- a/pyqtgraph/graphicsItems/tests/test_ImageItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -3,22 +3,22 @@ import pytest from pyqtgraph.Qt import QtCore, QtGui, QtTest import numpy as np import pyqtgraph as pg -from pyqtgraph.tests import assertImageApproved +from pyqtgraph.tests import assertImageApproved, TransposedImageItem app = pg.mkQApp() -def test_ImageItem(): +def test_ImageItem(transpose=False): w = pg.GraphicsWindow() view = pg.ViewBox() w.setCentralWidget(view) w.resize(200, 200) w.show() - img = pg.ImageItem(border=0.5) + img = TransposedImageItem(border=0.5, transpose=transpose) + view.addItem(img) - # test mono float np.random.seed(0) data = np.random.normal(size=(20, 20)) @@ -60,6 +60,18 @@ def test_ImageItem(): img.setLevels([127, 128]) assertImageApproved(w, 'imageitem/gradient_mono_byte_levels', 'Mono byte gradient w/ levels to isolate diagonal.') + # test monochrome image + data = np.zeros((10, 10), dtype='uint8') + data[:5,:5] = 1 + data[5:,5:] = 1 + img.setImage(data) + assertImageApproved(w, 'imageitem/monochrome', 'Ubyte image with only 0,1 values.') + + # test bool + data = data.astype(bool) + img.setImage(data) + assertImageApproved(w, 'imageitem/bool', 'Boolean mask.') + # test RGBA byte data = np.zeros((100, 100, 4), dtype='ubyte') data[..., 0] = np.linspace(0, 255, 100).reshape(100, 1) @@ -77,7 +89,7 @@ def test_ImageItem(): assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.') # checkerboard to test alpha - img2 = pg.ImageItem() + img2 = TransposedImageItem(transpose=transpose) img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2]) view.addItem(img2) img2.scale(10, 10) @@ -103,9 +115,23 @@ def test_ImageItem(): img.setAutoDownsample(True) assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.') + assert img._lastDownsample == (4, 1) img.setImage(data.T, levels=[-1, 1]) assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') + assert img._lastDownsample == (1, 4) + + view.hide() + +def test_ImageItem_axisorder(): + # All image tests pass again using the opposite axis order + origMode = pg.getConfigOption('imageAxisOrder') + altMode = 'row-major' if origMode == 'col-major' else 'col-major' + pg.setConfigOptions(imageAxisOrder=altMode) + try: + test_ImageItem(transpose=True) + finally: + pg.setConfigOptions(imageAxisOrder=origMode) @pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait") diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 973d8f1a..9e67fb8d 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -2,13 +2,13 @@ import numpy as np import pytest import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtTest -from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick +from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem app = pg.mkQApp() -def test_getArrayRegion(): +def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) rois = [ @@ -21,10 +21,23 @@ def test_getArrayRegion(): # For some ROIs, resize should not be used. testResize = not isinstance(roi, pg.PolyLineROI) - check_getArrayRegion(roi, 'roi/'+name, testResize) + origMode = pg.getConfigOption('imageAxisOrder') + try: + if transpose: + pg.setConfigOptions(imageAxisOrder='row-major') + check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True) + else: + pg.setConfigOptions(imageAxisOrder='col-major') + check_getArrayRegion(roi, 'roi/'+name, testResize) + finally: + pg.setConfigOptions(imageAxisOrder=origMode) + +def test_getArrayRegion_axisorder(): + test_getArrayRegion(transpose=True) + -def check_getArrayRegion(roi, name, testResize=True): +def check_getArrayRegion(roi, name, testResize=True, transpose=False): initState = roi.getState() #win = pg.GraphicsLayoutWidget() @@ -50,6 +63,7 @@ def check_getArrayRegion(roi, name, testResize=True): img1 = pg.ImageItem(border='w') img2 = pg.ImageItem(border='w') + vb1.addItem(img1) vb2.addItem(img2) @@ -60,6 +74,9 @@ def check_getArrayRegion(roi, name, testResize=True): data[:, :, 2, :] += 10 data[:, :, :, 3] += 10 + if transpose: + data = data.transpose(0, 2, 1, 3) + img1.setImage(data[0, ..., 0]) vb1.setAspectLocked() vb1.enableAutoRange(True, True) @@ -67,8 +84,14 @@ def check_getArrayRegion(roi, name, testResize=True): roi.setZValue(10) vb1.addItem(roi) + if isinstance(roi, pg.RectROI): + if transpose: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + else: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) - assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) + #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) img2.setImage(rgn[0, ..., 0]) vb2.setAspectLocked() vb2.enableAutoRange(True, True) @@ -122,6 +145,9 @@ def check_getArrayRegion(roi, name, testResize=True): img2.setImage(rgn[0, ..., 0]) app.processEvents() assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') + + # allow the roi to be re-used + roi.scene().removeItem(roi) def test_PolyLineROI(): diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 9f8d5ffc..5cc00f68 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -30,6 +30,7 @@ from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from .. import ptime as ptime from .. import debug as debug from ..SignalProxy import SignalProxy +from .. import getConfigOption try: from bottleneck import nanmin, nanmax @@ -203,9 +204,10 @@ class ImageView(QtGui.QWidget): """ Set the image to be displayed in the widget. - ================== ======================================================================= + ================== =========================================================================== **Arguments:** - img (numpy array) the image to be displayed. + img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and + *notes* below. xvals (numpy array) 1D array of z-axis values corresponding to the third axis in a 3D image. For video, this array should contain the time of each frame. autoRange (bool) whether to scale/pan the view to fit the image. @@ -222,7 +224,19 @@ class ImageView(QtGui.QWidget): and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. - ================== ======================================================================= + ================== =========================================================================== + + **Notes:** + + For backward compatibility, image data is assumed to be in column-major order (column, row). + However, most image data is stored in row-major order (row, column) and will need to be + transposed before calling setImage():: + + imageview.setImage(imagedata.T) + + This requirement can be changed by the ``imageAxisOrder`` + :ref:`global configuration option `. + """ profiler = debug.Profiler() @@ -239,28 +253,22 @@ class ImageView(QtGui.QWidget): self.image = img self.imageDisp = None - if xvals is not None: - self.tVals = xvals - elif hasattr(img, 'xvals'): - try: - self.tVals = img.xvals(0) - except: - self.tVals = np.arange(img.shape[0]) - else: - self.tVals = np.arange(img.shape[0]) - profiler() if axes is None: + x,y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0) + if img.ndim == 2: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} + self.axes = {'t': None, 'x': x, 'y': y, 'c': None} elif img.ndim == 3: + # Ambiguous case; make a guess if img.shape[2] <= 4: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} + self.axes = {'t': None, 'x': x, 'y': y, 'c': 2} else: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} + self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': None} elif img.ndim == 4: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} + # Even more ambiguous; just assume the default + self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': 3} else: raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): @@ -274,6 +282,18 @@ class ImageView(QtGui.QWidget): for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) + axes = self.axes + + if xvals is not None: + self.tVals = xvals + elif axes['t'] is not None: + if hasattr(img, 'xvals'): + try: + self.tVals = img.xvals(axes['t']) + except: + self.tVals = np.arange(img.shape[axes['t']]) + else: + self.tVals = np.arange(img.shape[axes['t']]) profiler() @@ -455,7 +475,7 @@ class ImageView(QtGui.QWidget): def setCurrentIndex(self, ind): """Set the currently displayed frame index.""" - self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) + self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) self.updateImage() self.ignoreTimeLine = True self.timeLine.setValue(self.tVals[self.currentIndex]) @@ -543,6 +563,7 @@ class ImageView(QtGui.QWidget): axes = (1, 2) else: return + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) if data is not None: while data.ndim > 1: @@ -638,11 +659,21 @@ class ImageView(QtGui.QWidget): if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) - if self.axes['t'] is None: - self.imageItem.updateImage(image) + + # Transpose image into order expected by ImageItem + if self.imageItem.axisOrder == 'col-major': + axorder = ['t', 'x', 'y', 'c'] else: + axorder = ['t', 'y', 'x', 'c'] + axorder = [self.axes[ax] for ax in axorder if self.axes[ax] is not None] + image = image.transpose(axorder) + + # Select time index + if self.axes['t'] is not None: self.ui.roiPlot.show() - self.imageItem.updateImage(image[self.currentIndex]) + image = image[self.currentIndex] + + self.imageItem.updateImage(image) def timeIndex(self, slider): diff --git a/pyqtgraph/imageview/tests/test_imageview.py b/pyqtgraph/imageview/tests/test_imageview.py index 2ca1712c..3057a8a5 100644 --- a/pyqtgraph/imageview/tests/test_imageview.py +++ b/pyqtgraph/imageview/tests/test_imageview.py @@ -7,5 +7,6 @@ def test_nan_image(): img = np.ones((10,10)) img[0,0] = np.nan v = pg.image(img) + v.imageItem.getHistogram() app.processEvents() v.window().close() diff --git a/pyqtgraph/tests/__init__.py b/pyqtgraph/tests/__init__.py index b755c384..a4fc235a 100644 --- a/pyqtgraph/tests/__init__.py +++ b/pyqtgraph/tests/__init__.py @@ -1,2 +1,2 @@ -from .image_testing import assertImageApproved +from .image_testing import assertImageApproved, TransposedImageItem from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index bab3acc4..f4404671 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -42,7 +42,7 @@ Procedure for unit-testing with images: # pyqtgraph should be tested against. When adding or changing test images, # create and push a new tag and update this variable. To test locally, begin # by creating the tag in your ~/.pyqtgraph/test-data repository. -testDataTag = 'test-data-5' +testDataTag = 'test-data-6' import time @@ -67,6 +67,30 @@ from .. import ImageItem, TextItem tester = None +# Convenient stamp used for ensuring image orientation is correct +axisImg = [ + " 1 1 1 ", + " 1 1 1 1 1 1 ", + " 1 1 1 1 1 1 1 1 1 1", + " 1 1 1 1 1 ", + " 1 1 1 1 1 1 ", + " 1 1 ", + " 1 1 ", + " 1 ", + " ", + " 1 ", + " 1 ", + " 1 ", + "1 1 1 1 1 ", + "1 1 1 1 1 ", + " 1 1 1 ", + " 1 1 1 ", + " 1 ", + " 1 ", +] +axisImg = np.array([map(int, row[::2].replace(' ', '0')) for row in axisImg]) + + def getTester(): global tester @@ -159,12 +183,15 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): print(graphstate) + + if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': + raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") except Exception: if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " "%s`\n" % (stdFileName, dataPath, standardFile)) - if os.getenv('PYQTGRAPH_AUDIT') == '1': + if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': sys.excepthook(*sys.exc_info()) getTester().test(image, stdImage, message) stdPath = os.path.dirname(stdFileName) @@ -344,7 +371,7 @@ class ImageTester(QtGui.QWidget): for i, v in enumerate(self.views): v.setAspectLocked(1) v.invertY() - v.image = ImageItem() + v.image = ImageItem(axisOrder='row-major') v.image.setAutoDownsample(True) v.addItem(v.image) v.label = TextItem(labelText[i]) @@ -371,9 +398,9 @@ class ImageTester(QtGui.QWidget): message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) self.label.setText(message) - self.views[0].image.setImage(im1.transpose(1, 0, 2)) - self.views[1].image.setImage(im2.transpose(1, 0, 2)) - diff = makeDiffImage(im1, im2).transpose(1, 0, 2) + self.views[0].image.setImage(im1) + self.views[1].image.setImage(im2) + diff = makeDiffImage(im1, im2) self.views[2].image.setImage(diff) self.views[0].autoRange() @@ -584,3 +611,15 @@ def transformStr(t): def indent(s, pfx): return '\n'.join([pfx+line for line in s.split('\n')]) + + +class TransposedImageItem(ImageItem): + # used for testing image axis order; we can test row-major and col-major using + # the same test images + def __init__(self, *args, **kwds): + self.__transpose = kwds.pop('transpose', False) + ImageItem.__init__(self, *args, **kwds) + def setImage(self, image=None, **kwds): + if image is not None and self.__transpose is True: + image = np.swapaxes(image, 0, 1) + return ImageItem.setImage(self, image, **kwds) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index efde07a4..f3f8cbb5 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -63,7 +63,7 @@ class GraphicsView(QtGui.QGraphicsView): :func:`mkColor `. By default, the background color is determined using the 'backgroundColor' configuration option (see - :func:`setConfigOption `. + :func:`setConfigOptions `). ============== ============================================================ """